From e82d71597ed5eb67379b59eb3ca46418e2dd50e1 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Mon, 18 May 2026 19:06:02 +0200 Subject: [PATCH 1/3] feat: enhance call management with active call tracking and improved flow handling --- .../kalium/persistence/ConversationDetails.sq | 2 - .../ConversationDetailsWithEvents.sq | 19 ++- .../kalium/persistence/dao/call/CallDAO.kt | 2 +- .../persistence/dao/call/CallDAOImpl.kt | 6 +- .../dao/conversation/ConversationDAOImpl.kt | 1 + .../ConversationDetailsWithEventsMapper.kt | 3 - .../conversation/ConversationExtensions.kt | 4 + .../dao/conversation/ConversationMapper.kt | 3 - .../conversation/ConversationViewEntity.kt | 2 - .../persistence/dao/ConversationDAOTest.kt | 5 +- ...tailsWithEventsBySearchQueryUseCaseTest.kt | 22 ++- .../kalium/logic/data/call/CallRepository.kt | 161 +++++++++++++----- .../data/conversation/ConversationMapper.kt | 4 +- .../ConversationRepositoryExtensions.kt | 5 + .../kalium/logic/feature/UserSessionScope.kt | 2 +- .../ActiveCallConversationMapper.kt | 41 +++++ .../feature/conversation/ConversationScope.kt | 11 +- .../ConversationScopeExtensions.kt | 2 +- ...onDetailsWithEventsBySearchQueryUseCase.kt | 38 ++++- ...rsationListDetailsWithEventsUseCaseImpl.kt | 38 +++-- .../ObserveConversationsFromFolderUseCase.kt | 20 ++- .../logic/data/call/CallRepositoryTest.kt | 156 ++++++++++------- .../ConversationRepositoryExtensionsTest.kt | 1 + ...serveConversationsFromFolderUseCaseTest.kt | 11 +- .../logic/framework/TestConversation.kt | 2 - 25 files changed, 395 insertions(+), 166 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ActiveCallConversationMapper.kt diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq index 237d29c17ab6..94e3d45c10f8 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq @@ -7,7 +7,6 @@ CASE (Conversation.type) ELSE Conversation.name END AS name, Conversation.type, -Call.status AS callStatus, CASE (Conversation.type) WHEN 'ONE_ON_ONE' THEN User.preview_asset_id WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id @@ -128,7 +127,6 @@ LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualifi OR Connection.status = 'NOT_CONNECTED' AND Conversation.type IS 'CONNECTION_PENDING') LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id -LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id ORDER BY created_at DESC LIMIT 1) AND Call.status IS 'STILL_ONGOING' LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type IS 'FAVORITE' LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id LEFT JOIN LabeledConversation AS ConversationLabel ON ConversationLabel.conversation_id = Conversation.qualified_id AND ConversationLabel.folder_id IS NOT FavoriteFolder.id diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq index c201914be627..5631338deddb 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq @@ -8,7 +8,6 @@ SELECT UnreadEventCountsGrouped.repliesCount AS unreadRepliesCount, UnreadEventCountsGrouped.messagesCount AS unreadMessagesCount, CASE - WHEN ConversationDetails.callStatus = 'STILL_ONGOING' AND ConversationDetails.type = 'GROUP' THEN 1 -- if ongoing call in a group, move it to the top WHEN ConversationDetails.mutedStatus = 'ALL_ALLOWED' THEN CASE WHEN (UnreadEventCountsGrouped.knocksCount + UnreadEventCountsGrouped.missedCallsCount + UnreadEventCountsGrouped.mentionsCount + UnreadEventCountsGrouped.repliesCount + UnreadEventCountsGrouped.messagesCount) > 0 THEN 1 -- if any unread events, move it to the top @@ -91,7 +90,11 @@ WHERE archived = :fromArchive ) AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END ORDER BY - CASE WHEN :newActivitiesOnTop THEN hasNewActivitiesToShow ELSE 0 END DESC, + CASE + WHEN :newActivitiesOnTop THEN + CASE WHEN qualifiedId IN :activeCallConversationIds THEN 1 ELSE hasNewActivitiesToShow END + ELSE 0 + END DESC, lastModifiedDate DESC, name IS NULL, name COLLATE NOCASE ASC; @@ -123,7 +126,11 @@ WHERE END AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END ORDER BY - CASE WHEN :newActivitiesOnTop THEN hasNewActivitiesToShow ELSE 0 END DESC, + CASE + WHEN :newActivitiesOnTop THEN + CASE WHEN qualifiedId IN :activeCallConversationIds THEN 1 ELSE hasNewActivitiesToShow END + ELSE 0 + END DESC, lastModifiedDate DESC, name IS NULL, name COLLATE NOCASE ASC @@ -158,7 +165,11 @@ WHERE AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END AND name LIKE ('%' || :searchQuery || '%') ORDER BY - CASE WHEN :newActivitiesOnTop THEN hasNewActivitiesToShow ELSE 0 END DESC, + CASE + WHEN :newActivitiesOnTop THEN + CASE WHEN qualifiedId IN :activeCallConversationIds THEN 1 ELSE hasNewActivitiesToShow END + ELSE 0 + END DESC, lastModifiedDate DESC, name IS NULL, name COLLATE NOCASE ASC diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAO.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAO.kt index 420e1f03e264..7facb31dc309 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAO.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAO.kt @@ -52,7 +52,7 @@ data class CallEntity( @Suppress("TooManyFunctions") interface CallDAO { - suspend fun insertCall(call: CallEntity) + suspend fun insertCall(call: CallEntity, createdAt: String? = null) suspend fun observeCalls(): Flow> suspend fun observeIncomingCalls(): Flow> suspend fun observeOutgoingCalls(): Flow> diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAOImpl.kt index c044d586ff49..4be569e1726e 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAOImpl.kt @@ -69,9 +69,9 @@ internal class CallDAOImpl( private val mapper: CallMapper = CallMapper, ) : CallDAO { - override suspend fun insertCall(call: CallEntity) { + override suspend fun insertCall(call: CallEntity, createdAt: String?) { withContext(writeDispatcher.value) { - val createdTime: Long = DateTimeUtil.currentInstant().toEpochMilliseconds() + val createdTime = createdAt ?: DateTimeUtil.currentInstant().toEpochMilliseconds().toString() callsQueries.insertCall( conversation_id = call.conversationId, @@ -79,7 +79,7 @@ internal class CallDAOImpl( status = call.status, caller_id = call.callerId, conversation_type = call.conversationType, - created_at = createdTime.toString(), + created_at = createdTime, type = call.type ) } diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index 995dbe89e157..470b53530731 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -348,6 +348,7 @@ internal class ConversationDAOImpl internal constructor( fromArchive = fromArchive, onlyInteractionsEnabled = onlyInteractionEnabled, newActivitiesOnTop = newActivitiesOnTop, + activeCallConversationIds = emptyList(), strict_mls = if (strictMLSFilter) 1 else 0, mapper = conversationDetailsWithEventsMapper::fromViewToModel ).asFlow() diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt index 97ac58d45de6..f1c3d9e0cf68 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt @@ -23,7 +23,6 @@ import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.SupportedProtocolEntity import com.wire.kalium.persistence.dao.UserAvailabilityStatusEntity import com.wire.kalium.persistence.dao.UserTypeEntity -import com.wire.kalium.persistence.dao.call.CallEntity import com.wire.kalium.persistence.dao.member.MemberEntity import com.wire.kalium.persistence.dao.message.MessageEntity import com.wire.kalium.persistence.dao.message.MessageMapper @@ -38,7 +37,6 @@ data object ConversationDetailsWithEventsMapper { qualifiedId: QualifiedIDEntity, name: String?, type: ConversationEntity.Type, - callStatus: CallEntity.Status?, previewAssetId: QualifiedIDEntity?, mutedStatus: ConversationEntity.MutedStatus, teamId: String?, @@ -120,7 +118,6 @@ data object ConversationDetailsWithEventsMapper { qualifiedId = qualifiedId, name = name, type = type, - callStatus = callStatus, previewAssetId = previewAssetId, mutedStatus = mutedStatus, teamId = teamId, diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt index 7bb8e0b77a8b..3b69ac52d5bc 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt @@ -21,6 +21,7 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import app.cash.sqldelight.paging3.QueryPagingSource import com.wire.kalium.persistence.ConversationDetailsWithEventsQueries +import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig import com.wire.kalium.persistence.dao.message.KaliumPager import com.wire.kalium.persistence.db.ReadDispatcher @@ -37,6 +38,7 @@ interface ConversationExtensions { val fromArchive: Boolean = false, val onlyInteractionEnabled: Boolean = false, val newActivitiesOnTop: Boolean = false, + val activeCallConversationIds: List = emptyList(), val conversationFilter: ConversationFilterEntity = ConversationFilterEntity.ALL, val strictMlsFilter: Boolean = true, ) @@ -100,6 +102,7 @@ internal class ConversationExtensionsImpl internal constructor( onlyInteractionsEnabled = onlyInteractionEnabled, conversationFilter = conversationFilter.name, newActivitiesOnTop = newActivitiesOnTop, + activeCallConversationIds = activeCallConversationIds, limit = limit, offset = offset, strict_mls = if (queryConfig.strictMlsFilter) 1 else 0, @@ -112,6 +115,7 @@ internal class ConversationExtensionsImpl internal constructor( conversationFilter = conversationFilter.name, searchQuery = searchQuery, newActivitiesOnTop = newActivitiesOnTop, + activeCallConversationIds = activeCallConversationIds, limit = limit, offset = offset, strict_mls = if (queryConfig.strictMlsFilter) 1 else 0, diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt index c77bfcf66eb5..f992153e500e 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt @@ -23,7 +23,6 @@ import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.SupportedProtocolEntity import com.wire.kalium.persistence.dao.UserAvailabilityStatusEntity import com.wire.kalium.persistence.dao.UserTypeEntity -import com.wire.kalium.persistence.dao.call.CallEntity import com.wire.kalium.persistence.dao.member.MemberEntity import kotlinx.datetime.Instant @@ -34,7 +33,6 @@ data object ConversationMapper { qualifiedId: QualifiedIDEntity, name: String?, type: ConversationEntity.Type, - callStatus: CallEntity.Status?, previewAssetId: QualifiedIDEntity?, mutedStatus: ConversationEntity.MutedStatus, teamId: String?, @@ -112,7 +110,6 @@ data object ConversationMapper { mlsLastKeyingMaterialUpdateDate = mlsLastKeyingMaterialUpdateDate, mlsGroupState = mlsGroupState, mlsProposalTimer = mlsProposalTimer, - callStatus = callStatus, previewAssetId = previewAssetId, userAvailabilityStatus = userAvailabilityStatus, userType = userType, diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt index 2fe6dbabc41c..19e1b1281a46 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt @@ -25,7 +25,6 @@ import com.wire.kalium.persistence.dao.SupportedProtocolEntity import com.wire.kalium.persistence.dao.UserAvailabilityStatusEntity import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.dao.UserTypeEntity -import com.wire.kalium.persistence.dao.call.CallEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity.ChannelAccess import com.wire.kalium.persistence.dao.conversation.ConversationEntity.ChannelAddPermission import com.wire.kalium.persistence.dao.member.MemberEntity @@ -36,7 +35,6 @@ data class ConversationViewEntity( val id: QualifiedIDEntity, val name: String?, val type: ConversationEntity.Type, - val callStatus: CallEntity.Status?, val previewAssetId: QualifiedIDEntity?, val mutedStatus: ConversationEntity.MutedStatus, val teamId: String?, diff --git a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt index ae228ea01a03..59e56ef16214 100644 --- a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt +++ b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt @@ -2794,7 +2794,6 @@ class ConversationDAOTest : BaseDatabaseTest() { id = id, name = if (type == ConversationEntity.Type.ONE_ON_ONE) userEntity?.name else name, type = type, - callStatus = null, previewAssetId = null, mutedStatus = mutedStatus, teamId = if (type == ConversationEntity.Type.ONE_ON_ONE) userEntity?.team else teamId, @@ -3132,7 +3131,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenSomePreviousCallIsWronglyStillOngoingButLastOneIsAlreadyClosed_whenFetchingConversationDetails_thenReturnStateOfLastCall() = + fun givenCallsExist_whenFetchingConversationDetails_thenReturnConversationDetailsWithoutCallState() = runTest(dispatcher) { val conversationEntity1 = conversationEntity1.copy( id = ConversationIDEntity("conversation1", "domain"), @@ -3146,7 +3145,7 @@ class ConversationDAOTest : BaseDatabaseTest() { callDAO.insertCall(callEntity1.copy(id = "2", status = CallEntity.Status.CLOSED)) // last call already closed conversationDAO.getConversationDetailsById(conversationEntity1.id).let { assertNotNull(it) - assertEquals(null, it.callStatus) // no call status because the last call is already closed + assertEquals(conversationEntity1.id, it.id) } } diff --git a/logic/src/androidHostTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt b/logic/src/androidHostTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt index 337a87e1cb9b..b1e584bb6bdc 100644 --- a/logic/src/androidHostTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt +++ b/logic/src/androidHostTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.feature.conversation import androidx.paging.PagingConfig import androidx.paging.PagingData +import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationQueryConfig import com.wire.kalium.logic.data.conversation.ConversationRepository @@ -32,7 +33,8 @@ import dev.mokkery.mock import dev.mokkery.verify.VerifyMode import dev.mokkery.verifySuspend import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test @@ -42,14 +44,14 @@ internal class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCa @Test fun givenSearchQuery_whenGettingPaginatedList_thenCallUseCaseWithProperParams() = runTest(dispatcher.default) { // Given - val (arrangement, useCase) = Arrangement().withPaginatedConversationResult(emptyFlow()).arrange() + val (arrangement, useCase) = Arrangement().withPaginatedConversationResult(flowOf(PagingData.empty())).arrange() with(arrangement) { // When - useCase(queryConfig = queryConfig, pagingConfig = pagingConfig, startingOffset = startingOffset, strictMlsFilter = false) + useCase(queryConfig = queryConfig, pagingConfig = pagingConfig, startingOffset = startingOffset, strictMlsFilter = false).first() // Then verifySuspend(VerifyMode.exactly(1)) { conversationRepository.extensions - .getPaginatedConversationDetailsWithEventsBySearchQuery(queryConfig, pagingConfig, startingOffset, false) + .getPaginatedConversationDetailsWithEventsBySearchQuery(queryConfig, emptyList(), pagingConfig, startingOffset, false) } } } @@ -57,6 +59,7 @@ internal class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCa inner class Arrangement { val conversationRepository = mock() val conversationRepositoryExtensions = mock() + val callRepository = mock() val queryConfig = ConversationQueryConfig("search") val pagingConfig = PagingConfig(20) @@ -66,14 +69,21 @@ internal class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCa every { conversationRepository.extensions }.returns(conversationRepositoryExtensions) + everySuspend { + callRepository.ongoingCallsFlow() + } returns flowOf(emptyList()) } suspend fun withPaginatedConversationResult(result: Flow>) = apply { everySuspend { - conversationRepositoryExtensions.getPaginatedConversationDetailsWithEventsBySearchQuery(any(), any(), any(), any()) + conversationRepositoryExtensions.getPaginatedConversationDetailsWithEventsBySearchQuery(any(), any(), any(), any(), any()) } returns result } - fun arrange() = this to GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(dispatcher, conversationRepository) + fun arrange() = this to GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase( + dispatcher, + conversationRepository, + callRepository + ) } } 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 447f875596fc..9c5059690f36 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 @@ -132,7 +132,6 @@ internal interface CallRepository { suspend fun updateCallParticipants(conversationId: ConversationId, participants: List) fun updateParticipantsActiveSpeaker(conversationId: ConversationId, activeSpeakers: Map>) suspend fun getLastClosedCallCreatedByConversationId(conversationId: ConversationId): Flow - suspend fun updateOpenCallsToClosedStatus(setStaleOpenCallsCleanupFinishedAfterwards: Boolean = true) fun setStaleOpenCallsCleanupFinished() fun observeStaleOpenCallsCleanupFinished(): Flow suspend fun leavePreviouslyJoinedMlsConferences() @@ -188,6 +187,8 @@ internal class CallDataSource( private val _recentlyEndedCallFlow = MutableSharedFlow( extraBufferCapacity = 1 ) + private val activeCallEntities = MutableStateFlow>(emptyList()) + private val activeCallCreatedAt = ConcurrentMutableMap() private val staleOpenCallsCleanupFinishedFlow = MutableStateFlow(false) override suspend fun updateRecentlyEndedCallMetadata(recentlyEndedCallMetadata: RecentlyEndedCallMetadata) { @@ -208,14 +209,32 @@ internal class CallDataSource( override fun getCallMetadata(conversationId: ConversationId): CallMetadata? = _callMetadataProfile[conversationId] - override suspend fun callsFlow(): Flow> = callDAO.observeCalls().combineWithCallsMetadata() + override suspend fun callsFlow(): Flow> = + callDAO.observeCalls() + .combine(activeCallEntities) { persistedCalls, activeCalls -> + persistedCalls.filterNot { it.status.isActive } + activeCalls + } + .combineWithCallsMetadata() - override suspend fun incomingCallsFlow(): Flow> = callDAO.observeIncomingCalls().combineWithCallsMetadata() - override suspend fun outgoingCallsFlow(): Flow> = callDAO.observeOutgoingCalls().combineWithCallsMetadata() + override suspend fun incomingCallsFlow(): Flow> = + activeCallEntities + .map { calls -> calls.filter { it.status == CallEntity.Status.INCOMING } } + .combineWithCallsMetadata() - override suspend fun ongoingCallsFlow(): Flow> = callDAO.observeOngoingCalls().combineWithCallsMetadata() + override suspend fun outgoingCallsFlow(): Flow> = + activeCallEntities + .map { calls -> calls.filter { it.status == CallEntity.Status.STARTED } } + .combineWithCallsMetadata() - override suspend fun establishedCallsFlow(): Flow> = callDAO.observeEstablishedCalls().combineWithCallsMetadata() + override suspend fun ongoingCallsFlow(): Flow> = + activeCallEntities + .map { calls -> calls.filter { it.status == CallEntity.Status.STILL_ONGOING } } + .combineWithCallsMetadata() + + override suspend fun establishedCallsFlow(): Flow> = + activeCallEntities + .map { calls -> calls.filter { it.status == CallEntity.Status.ESTABLISHED || it.status == CallEntity.Status.ANSWERED } } + .combineWithCallsMetadata() private val mutexProvider = MutexProvider() @@ -269,7 +288,10 @@ internal class CallDataSource( ) val isCallInCurrentSession = _callMetadataProfile.value.containsKey(conversationId) - val lastCallStatus = callDAO.getCallStatusByConversationId(conversationId = callEntity.conversationId) + val lastCallStatus = activeCallEntities.value + .lastOrNull { it.conversationId == callEntity.conversationId } + ?.status + ?: callDAO.getCallStatusByConversationId(conversationId = callEntity.conversationId)?.takeUnless { it.isActive } val isOneOnOneCall = callEntity.conversationType == ConversationEntity.Type.ONE_ON_ONE val isGroupCall = callEntity.conversationType == ConversationEntity.Type.GROUP @@ -306,10 +328,7 @@ internal class CallDataSource( "| isGroupCall: [$isGroupCall] | isOneOnOneCall: [$isOneOnOneCall]" ) - // Save into database - wrapStorageRequest { - callDAO.insertCall(call = callEntity) - } + upsertActiveCall(callEntity) } } else { callingLogger.i( @@ -328,31 +347,52 @@ internal class CallDataSource( callMetadataProfile.plus(conversationId = conversationId, metadata = metadata) } - // Save into database - wrapStorageRequest { - callDAO.insertCall(call = callEntity) - } + upsertActiveCall(callEntity) } } } override suspend fun updateCallStatusById(conversationId: ConversationId, status: CallStatus) { - // Update Call in Database - wrapStorageRequest { - callDAO.updateLastCallStatusByConversationId( - status = callMapper.toCallEntityStatus(callStatus = status), - conversationId = callMapper.fromConversationIdToQualifiedIDEntity( - conversationId = conversationId + val callEntityStatus = callMapper.toCallEntityStatus(callStatus = status) + val conversationIdEntity = callMapper.fromConversationIdToQualifiedIDEntity(conversationId = conversationId) + val activeCall = activeCallEntities.value.lastOrNull { it.conversationId == conversationIdEntity } + val updatedActiveCall = activeCall?.copy(status = callEntityStatus) + + if (updatedActiveCall != null) { + activeCallEntities.update { calls -> + when { + callEntityStatus.isActive -> calls.map { call -> if (call.id == updatedActiveCall.id) updatedActiveCall else call } + activeCall.shouldKeepGroupCallJoinable(callEntityStatus) -> calls.map { call -> + if (call.id == activeCall.id) activeCall.copy(status = CallEntity.Status.STILL_ONGOING) else call + } + + else -> calls.filterNot { call -> call.id == updatedActiveCall.id } + } + } + if (!callEntityStatus.isActive && !activeCall.shouldKeepGroupCallJoinable(callEntityStatus)) { + wrapStorageRequest { + callDAO.insertCall( + call = updatedActiveCall, + createdAt = activeCallCreatedAt.remove(updatedActiveCall.id) + ) + } + } + } else { + wrapStorageRequest { + callDAO.updateLastCallStatusByConversationId( + status = callEntityStatus, + conversationId = conversationIdEntity ) - ) - callingLogger.i( - "[CallRepository][UpdateCallStatusById] ->" + - " ConversationId: [${conversationId.value.obfuscateId()}" + - "@${conversationId.domain.obfuscateDomain()}]" + - " " + "| status: [$status]" - ) + } } + callingLogger.i( + "[CallRepository][UpdateCallStatusById] ->" + + " ConversationId: [${conversationId.value.obfuscateId()}" + + "@${conversationId.domain.obfuscateDomain()}]" + + " " + "| status: [$status]" + ) + _callMetadataProfile.update(conversationId) { callMetadata -> callMetadata.copy( callStatus = status, @@ -364,13 +404,18 @@ internal class CallDataSource( } } + private fun CallEntity.shouldKeepGroupCallJoinable(status: CallEntity.Status): Boolean = + conversationType == ConversationEntity.Type.GROUP && status != CallEntity.Status.CLOSED + override suspend fun persistMissedCall(conversationId: ConversationId) { callingLogger.i( "[CallRepository] -> Persisting Missed Call for conversation : conversationId: " + conversationId.toLogString() ) val qualifiedIDEntity = callMapper.fromConversationIdToQualifiedIDEntity(conversationId = conversationId) - callDAO.getCallerIdByConversationId(conversationId = qualifiedIDEntity)?.let { callerId -> + (_callMetadataProfile.value[conversationId]?.callerId?.toString() ?: callDAO.getCallerIdByConversationId( + conversationId = qualifiedIDEntity + ))?.let { callerId -> val qualifiedUserId = qualifiedIdMapper.fromStringToQualifiedID(callerId) val message = Message.System( @@ -394,7 +439,11 @@ internal class CallDataSource( } override suspend fun updateIsCbrEnabled(isCbrEnabled: Boolean) { - val conversationId = callDAO.getEstablishedCall().conversationId.toModel() + val conversationId = activeCallEntities.value + .firstOrNull { it.status == CallEntity.Status.ESTABLISHED || it.status == CallEntity.Status.ANSWERED } + ?.conversationId + ?.toModel() + ?: callDAO.getEstablishedCall().conversationId.toModel() _callMetadataProfile.update(conversationId) { callMetadata -> callMetadata.copy(isCbrEnabled = isCbrEnabled) } @@ -547,16 +596,6 @@ internal class CallDataSource( ) ) - override suspend fun updateOpenCallsToClosedStatus(setStaleOpenCallsCleanupFinishedAfterwards: Boolean) { - leavePreviouslyJoinedMlsConferences() - val count = callDAO.observeEstablishedCalls().first().size - callingLogger.i("Updating open calls to closed status if there are any $count") - callDAO.updateOpenCallsToClosedStatus() - if (setStaleOpenCallsCleanupFinishedAfterwards) { - setStaleOpenCallsCleanupFinished() - } - } - override fun setStaleOpenCallsCleanupFinished() { staleOpenCallsCleanupFinishedFlow.value = true callingLogger.i("Stale open calls cleanup is done") @@ -564,8 +603,8 @@ internal class CallDataSource( override fun observeStaleOpenCallsCleanupFinished(): Flow = staleOpenCallsCleanupFinishedFlow override suspend fun establishedCallConversationId(): ConversationId? = - callDAO - .observeEstablishedCalls() + activeCallEntities + .map { calls -> calls.filter { it.status == CallEntity.Status.ESTABLISHED || it.status == CallEntity.Status.ANSWERED } } .combineWithCallsMetadata() .first() .firstOrNull() @@ -586,7 +625,8 @@ internal class CallDataSource( override suspend fun leavePreviouslyJoinedMlsConferences() { callingLogger.i("Leaving previously joined MLS conferences") - callDAO.observeEstablishedCalls() + activeCallEntities + .map { calls -> calls.filter { it.status == CallEntity.Status.ESTABLISHED || it.status == CallEntity.Status.ANSWERED } } .first() .filter { it.type == CallEntity.Type.MLS_CONFERENCE } .forEach { @@ -739,7 +779,12 @@ internal class CallDataSource( } override fun observeLastActiveCallByConversationId(conversationId: ConversationId): Flow = - callDAO.observeLastActiveCallByConversationId(callMapper.fromConversationIdToQualifiedIDEntity(conversationId)) + activeCallEntities + .map { activeCalls -> + activeCalls.lastOrNull { + it.conversationId == callMapper.fromConversationIdToQualifiedIDEntity(conversationId) && it.status.isActive + } + } .combineWithCallMetadata() override fun updateCallQualityData(conversationId: ConversationId, callQualityData: CallQualityData) = @@ -748,6 +793,34 @@ internal class CallDataSource( override fun observeCallQualityData(conversationId: ConversationId) = _callQualityDataProfile.mapNotNull { it[conversationId] } + private fun upsertActiveCall(call: CallEntity) { + if (!activeCallCreatedAt.containsKey(call.id)) { + activeCallCreatedAt[call.id] = DateTimeUtil.currentInstant().toEpochMilliseconds().toString() + } + activeCallEntities.value + .filter { it.conversationId == call.conversationId && it.id != call.id } + .forEach { activeCallCreatedAt.remove(it.id) } + activeCallEntities.update { calls -> + calls + .filterNot { it.conversationId == call.conversationId } + .plus(call) + } + } + + private val CallEntity.Status.isActive: Boolean + get() = when (this) { + CallEntity.Status.STARTED, + CallEntity.Status.INCOMING, + CallEntity.Status.ANSWERED, + CallEntity.Status.ESTABLISHED, + CallEntity.Status.STILL_ONGOING -> true + + CallEntity.Status.MISSED, + CallEntity.Status.CLOSED_INTERNALLY, + CallEntity.Status.CLOSED, + CallEntity.Status.REJECTED -> false + } + companion object { val STALE_PARTICIPANT_TIMEOUT = 190.toDuration(kotlin.time.DurationUnit.SECONDS) } 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 a66e8b48c91a..f113a54716da 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 @@ -294,7 +294,7 @@ internal class ConversationMapperImpl( if (isChannel) { ConversationDetails.Group.Channel( conversation = fromConversationViewToEntity(daoModel), - hasOngoingCall = callStatus != null, // todo: we can do better! + hasOngoingCall = false, isSelfUserMember = isMember, selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }, isFavorite = isFavorite, @@ -310,7 +310,7 @@ internal class ConversationMapperImpl( } else { ConversationDetails.Group.Regular( conversation = fromConversationViewToEntity(daoModel), - hasOngoingCall = callStatus != null, // todo: we can do better! + hasOngoingCall = false, isSelfUserMember = isMember, selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }, isFavorite = isFavorite, 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 6f045512e764..e0f07958be86 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 @@ -23,6 +23,8 @@ import androidx.paging.map import com.wire.kalium.persistence.dao.conversation.ConversationDAO import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.persistence.dao.message.KaliumPager import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -30,6 +32,7 @@ import kotlinx.coroutines.flow.map internal interface ConversationRepositoryExtensions { suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( queryConfig: ConversationQueryConfig, + activeCallConversationIds: List, pagingConfig: PagingConfig, startingOffset: Long, strictMlsFilter: Boolean, @@ -42,6 +45,7 @@ internal class ConversationRepositoryExtensionsImpl internal constructor( ) : ConversationRepositoryExtensions { override suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( queryConfig: ConversationQueryConfig, + activeCallConversationIds: List, pagingConfig: PagingConfig, startingOffset: Long, strictMlsFilter: Boolean, @@ -53,6 +57,7 @@ internal class ConversationRepositoryExtensionsImpl internal constructor( fromArchive = fromArchive, onlyInteractionEnabled = onlyInteractionEnabled, newActivitiesOnTop = newActivitiesOnTop, + activeCallConversationIds = activeCallConversationIds.map { it.toDao() }, conversationFilter = conversationFilter.toDao(), strictMlsFilter = strictMlsFilter ), 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 449130b733f7..1f61a68ea453 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 @@ -2405,6 +2405,7 @@ public class UserSessionScope internal constructor( sessionManager.getServerConfig().links, messages.messageRepository, assetRepository, + callRepository, newGroupConversationSystemMessagesCreator, deleteConversationUseCase, persistConversationsUseCase, @@ -2934,7 +2935,6 @@ public class UserSessionScope internal constructor( @Suppress("TooGenericExceptionCaught") try { apiMigrationManager.performMigrations() - callRepository.updateOpenCallsToClosedStatus() messageRepository.resetAssetTransferStatus() } catch (exception: CancellationException) { throw exception diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ActiveCallConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ActiveCallConversationMapper.kt new file mode 100644 index 000000000000..0b6ab69e604f --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ActiveCallConversationMapper.kt @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2026 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.data.conversation.ConversationDetails +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.id.ConversationId + +internal fun ConversationDetailsWithEvents.withActiveCallStatus(activeCallConversationIds: Set): ConversationDetailsWithEvents { + val hasOngoingCall = conversationDetails.conversation.id in activeCallConversationIds + return copy( + conversationDetails = conversationDetails.withActiveCallStatus(hasOngoingCall), + hasNewActivitiesToShow = hasNewActivitiesToShow || hasOngoingCall + ) +} + +private fun ConversationDetails.withActiveCallStatus(hasOngoingCall: Boolean): ConversationDetails = + when (this) { + is ConversationDetails.Group.Regular -> copy(hasOngoingCall = hasOngoingCall) + is ConversationDetails.Group.Channel -> copy(hasOngoingCall = hasOngoingCall) + is ConversationDetails.Connection, + is ConversationDetails.OneOne, + is ConversationDetails.Self, + is ConversationDetails.Team -> this + } 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 4530986a2777..e4aa1ad330a9 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 @@ -24,6 +24,7 @@ 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.call.CallRepository import com.wire.kalium.logic.data.client.CryptoTransactionProvider import com.wire.kalium.logic.data.connection.ConnectionRepository import com.wire.kalium.logic.data.conversation.ConversationGroupRepository @@ -140,6 +141,7 @@ public class ConversationScope internal constructor( private val serverConfigLinks: ServerConfig.Links, internal val messageRepository: MessageRepository, internal val assetRepository: AssetRepository, + internal val callRepository: CallRepository, private val newGroupConversationSystemMessagesCreator: NewGroupConversationSystemMessagesCreator, private val deleteConversationUseCase: DeleteConversationUseCase, private val persistConversationsUseCase: PersistConversationsUseCase, @@ -165,7 +167,12 @@ public class ConversationScope internal constructor( get() = ObserveConversationListDetailsUseCaseImpl(conversationRepository) public val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase - get() = ObserveConversationListDetailsWithEventsUseCaseImpl(conversationRepository, conversationFolderRepository, getFavoriteFolder) + get() = ObserveConversationListDetailsWithEventsUseCaseImpl( + conversationRepository, + conversationFolderRepository, + getFavoriteFolder, + callRepository + ) public val observeConversationMembers: ObserveConversationMembersUseCase get() = ObserveConversationMembersUseCaseImpl(conversationRepository, userRepository) @@ -449,7 +456,7 @@ public class ConversationScope internal constructor( public val syncConversationCode: SyncConversationCodeUseCase get() = SyncConversationCodeUseCase(conversationGroupRepository, serverConfigLinks) public val observeConversationsFromFolder: ObserveConversationsFromFolderUseCase - get() = ObserveConversationsFromFolderUseCaseImpl(conversationFolderRepository) + get() = ObserveConversationsFromFolderUseCaseImpl(conversationFolderRepository, callRepository) public val getFavoriteFolder: GetFavoriteFolderUseCase get() = GetFavoriteFolderUseCaseImpl(conversationFolderRepository) public val addConversationToFavorites: AddConversationToFavoritesUseCase diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt index 871c601d5fe3..bff776f6152e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt @@ -19,4 +19,4 @@ package com.wire.kalium.logic.feature.conversation public val ConversationScope.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery: GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase - get() = GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(dispatcher, conversationRepository) + get() = GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(dispatcher, conversationRepository, callRepository) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt index cc757815f4ca..dd96d2f5e61f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt @@ -19,12 +19,19 @@ package com.wire.kalium.logic.feature.conversation import androidx.paging.PagingConfig import androidx.paging.PagingData +import androidx.paging.map +import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.ConversationQueryConfig import com.wire.kalium.util.KaliumDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart /** * This use case will observe and return a flow of paginated searched conversation details with last message and unread events counts. @@ -35,18 +42,33 @@ import kotlinx.coroutines.flow.flowOn public class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase internal constructor( private val dispatcher: KaliumDispatcher, private val conversationRepository: ConversationRepository, + private val callRepository: CallRepository, ) { + @OptIn(ExperimentalCoroutinesApi::class) public suspend operator fun invoke( queryConfig: ConversationQueryConfig, pagingConfig: PagingConfig, startingOffset: Long, strictMlsFilter: Boolean - ): Flow> = conversationRepository.extensions - .getPaginatedConversationDetailsWithEventsBySearchQuery( - queryConfig = queryConfig, - pagingConfig = pagingConfig, - startingOffset = startingOffset, - strictMlsFilter = strictMlsFilter - ) - .flowOn(dispatcher.io) + ): Flow> = + callRepository.ongoingCallsFlow() + .map { calls -> calls.map { it.conversationId }.toSet() } + .onStart { emit(emptySet()) } + .distinctUntilChanged() + .flatMapLatest { activeCallConversationIds -> + conversationRepository.extensions + .getPaginatedConversationDetailsWithEventsBySearchQuery( + queryConfig = queryConfig, + activeCallConversationIds = activeCallConversationIds.toList(), + pagingConfig = pagingConfig, + startingOffset = startingOffset, + strictMlsFilter = strictMlsFilter + ) + .map { pagingData -> + pagingData.map { conversation -> + conversation.withActiveCallStatus(activeCallConversationIds) + } + } + } + .flowOn(dispatcher.io) } 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 04198d1bb7aa..611e06cbba02 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 @@ -18,14 +18,20 @@ package com.wire.kalium.logic.feature.conversation +import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationFilter import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart /** * This use case will observe and return the list of conversation details for the current user. @@ -42,29 +48,25 @@ public interface ObserveConversationListDetailsWithEventsUseCase { internal class ObserveConversationListDetailsWithEventsUseCaseImpl( private val conversationRepository: ConversationRepository, private val conversationFolderRepository: ConversationFolderRepository, - private val getFavoriteFolder: GetFavoriteFolderUseCase + private val getFavoriteFolder: GetFavoriteFolderUseCase, + private val callRepository: CallRepository ) : ObserveConversationListDetailsWithEventsUseCase { + @OptIn(ExperimentalCoroutinesApi::class) override suspend operator fun invoke( fromArchive: Boolean, conversationFilter: ConversationFilter, strictMlsFilter: Boolean, ): Flow> { - return when (conversationFilter) { - ConversationFilter.Favorites -> { - when (val result = getFavoriteFolder()) { - GetFavoriteFolderUseCase.Result.Failure -> { - flowOf(emptyList()) - } - - is GetFavoriteFolderUseCase.Result.Success -> - conversationFolderRepository.observeConversationsFromFolder(result.folder.id) - } + val conversationsFlow = when (conversationFilter) { + ConversationFilter.Favorites -> when (val result = getFavoriteFolder()) { + GetFavoriteFolderUseCase.Result.Failure -> flowOf(emptyList()) + is GetFavoriteFolderUseCase.Result.Success -> + conversationFolderRepository.observeConversationsFromFolder(result.folder.id) } - is ConversationFilter.Folder -> { + is ConversationFilter.Folder -> conversationFolderRepository.observeConversationsFromFolder(conversationFilter.folderId) - } ConversationFilter.All, ConversationFilter.Channels, @@ -72,5 +74,15 @@ internal class ObserveConversationListDetailsWithEventsUseCaseImpl( ConversationFilter.OneOnOne -> conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter, strictMlsFilter) } + + return callRepository.ongoingCallsFlow() + .map { calls -> calls.map { it.conversationId }.toSet() } + .onStart { emit(emptySet()) } + .distinctUntilChanged() + .flatMapLatest { activeCallConversationIds -> + conversationsFlow.map { conversations -> + conversations.map { it.withActiveCallStatus(activeCallConversationIds) } + } + } } } 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 bc0994883115..7ce415bb7527 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 @@ -17,12 +17,19 @@ */ package com.wire.kalium.logic.feature.conversation.folder +import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.feature.conversation.withActiveCallStatus import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart /** * This use case will observe and return the list of conversations from given folder. @@ -34,10 +41,21 @@ public fun interface ObserveConversationsFromFolderUseCase { internal class ObserveConversationsFromFolderUseCaseImpl( private val conversationFolderRepository: ConversationFolderRepository, + private val callRepository: CallRepository, private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : ObserveConversationsFromFolderUseCase { + @OptIn(ExperimentalCoroutinesApi::class) override suspend operator fun invoke(folderId: String): Flow> = - conversationFolderRepository.observeConversationsFromFolder(folderId) + callRepository.ongoingCallsFlow() + .map { calls -> calls.map { it.conversationId }.toSet() } + .onStart { emit(emptySet()) } + .distinctUntilChanged() + .flatMapLatest { activeCallConversationIds -> + conversationFolderRepository.observeConversationsFromFolder(folderId) + .map { conversations -> + conversations.map { it.withActiveCallStatus(activeCallConversationIds) } + } + } .flowOn(dispatchers.io) } 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 ad89d79bb6f1..6352b26f80bc 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 @@ -719,6 +719,98 @@ class CallRepositoryTest { } } + @Test + fun givenAGroupCallIsOngoing_whenLeavingLocally_thenKeepItJoinableUntilItIsClosedByAvs() = runTest { + val (_, callRepository) = Arrangement(testDispatcher.testKaliumDispatcher()) + .givenObserveConversationDetailsByIdReturns( + flowOf( + Either.Right( + ConversationDetails.Group.Regular( + Arrangement.groupConversation, + isSelfUserMember = true, + selfRole = Conversation.Member.Role.Member + ) + ) + ) + ) + .givenGetKnownUserSucceeds() + .givenGetTeamSucceeds() + .givenGetCallStatusByConversationIdReturns(null) + .arrange() + + callRepository.createCall( + conversationId = Arrangement.conversationId, + status = CallStatus.STILL_ONGOING, + callerId = callerId, + isMuted = true, + isCameraOn = false, + isCbrEnabled = false, + type = ConversationTypeForCall.Conference + ) + callRepository.updateCallStatusById(Arrangement.conversationId, CallStatus.ESTABLISHED) + callRepository.updateCallStatusById(Arrangement.conversationId, CallStatus.CLOSED_INTERNALLY) + + callRepository.ongoingCallsFlow().test { + assertEquals(CallStatus.STILL_ONGOING, awaitItem().single().status) + cancelAndIgnoreRemainingEvents() + } + + callRepository.establishedCallsFlow().test { + assertEquals(emptyList(), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + + callRepository.updateCallStatusById(Arrangement.conversationId, CallStatus.CLOSED) + + callRepository.ongoingCallsFlow().test { + assertEquals(emptyList(), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun givenAGroupCallIsOngoing_whenRejectingLocally_thenKeepItJoinableUntilItIsClosedByAvs() = runTest { + val (_, callRepository) = Arrangement(testDispatcher.testKaliumDispatcher()) + .givenObserveConversationDetailsByIdReturns( + flowOf( + Either.Right( + ConversationDetails.Group.Regular( + Arrangement.groupConversation, + isSelfUserMember = true, + selfRole = Conversation.Member.Role.Member + ) + ) + ) + ) + .givenGetKnownUserSucceeds() + .givenGetTeamSucceeds() + .givenGetCallStatusByConversationIdReturns(null) + .arrange() + + callRepository.createCall( + conversationId = Arrangement.conversationId, + status = CallStatus.STILL_ONGOING, + callerId = callerId, + isMuted = true, + isCameraOn = false, + isCbrEnabled = false, + type = ConversationTypeForCall.Conference + ) + callRepository.updateCallStatusById(Arrangement.conversationId, CallStatus.REJECTED) + + callRepository.ongoingCallsFlow().test { + assertEquals(CallStatus.STILL_ONGOING, awaitItem().single().status) + cancelAndIgnoreRemainingEvents() + } + + callRepository.updateCallStatusById(Arrangement.conversationId, CallStatus.CLOSED) + + callRepository.ongoingCallsFlow().test { + assertEquals(emptyList(), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun givenAConversationIdThatDoesNotExistsInTheFlow_whenUpdateIsMutedByIdIsCalled_thenDoNotUpdateTheFlow() = runTest { val (_, callRepository) = Arrangement(testDispatcher.testKaliumDispatcher()).arrange() @@ -1473,31 +1565,6 @@ class CallRepositoryTest { } } - @Test - fun givenMlsConferenceCall_whenClosingOpenCalls_thenAttemptToLeaveMlsConference() = runTest { - // given - val callEntity = createCallEntity().copy( - status = CallEntity.Status.ESTABLISHED, - callerId = "callerId@domain", - type = CallEntity.Type.MLS_CONFERENCE - ) - val (arrangement, callRepository) = Arrangement(testDispatcher.testKaliumDispatcher()) - .givenObserveEstablishedCallsReturns(flowOf(listOf(callEntity))) - .givenLeaveSubconversationSuccessful() - .arrange() - - // when - callRepository.updateOpenCallsToClosedStatus() - yield() - advanceUntilIdle() - - // then - verifySuspend { - arrangement.leaveSubconversationUseCase.invoke(any(), Arrangement.conversationId, CALL_SUBCONVERSATION_ID) - } - - } - @Test fun givenStaleOpenCallsCleanupIsObserved_whenCleanupIsMarkedDone_thenInitialAndDoneStatesAreEmitted() = runTest { // given @@ -1515,45 +1582,6 @@ class CallRepositoryTest { } } - @Test - fun givenOpenCallsCleanupShouldNotBeMarkedDone_whenClosingOpenCalls_thenCleanupFinishedIsNotEmitted() = runTest { - // given - val (_, callRepository) = Arrangement(testDispatcher.testKaliumDispatcher()) - .givenObserveEstablishedCallsReturns(flowOf(emptyList())) - .arrange() - - callRepository.observeStaleOpenCallsCleanupFinished().test { - // then - assertEquals(false, awaitItem()) - - // when - callRepository.updateOpenCallsToClosedStatus(setStaleOpenCallsCleanupFinishedAfterwards = false) - - // then - expectNoEvents() - } - } - - @Test - fun givenOpenCallsCleanupShouldBeMarkedDone_whenClosingOpenCalls_thenCleanupFinishedIsEmitted() = runTest { - // given - val (_, callRepository) = Arrangement(testDispatcher.testKaliumDispatcher()) - .givenObserveEstablishedCallsReturns(flowOf(emptyList())) - .arrange() - - callRepository.observeStaleOpenCallsCleanupFinished().test { - // then - assertEquals(false, awaitItem()) - - // when - callRepository.updateOpenCallsToClosedStatus() - - // then - assertEquals(true, awaitItem()) - cancelAndIgnoreRemainingEvents() - } - } - @Test fun givenAConversationIdThatExistsInTheFlow_whenUpdateParticipantsActiveSpeakerIsCalled_thenUpdateTheFlow() = runTest { val (_, callRepository) = Arrangement(testDispatcher.testKaliumDispatcher()) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt index c687329ea661..1d9216d54e45 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt @@ -67,6 +67,7 @@ class ConversationRepositoryExtensionsTest { onlyInteractionEnabled = false, newActivitiesOnTop = false, ), + activeCallConversationIds = emptyList(), pagingConfig = pagingConfig, startingOffset = 0L, strictMlsFilter = false diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt index a7acfb430fd9..ba5de6e24df5 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.logic.feature.conversation.folder +import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.framework.TestConversationDetails @@ -79,11 +80,19 @@ class ObserveConversationsFromFolderUseCaseTest { private class Arrangement { val conversationFolderRepository = mock(mode = MockMode.autoUnit) + val callRepository = mock() private val observeConversationsFromFolderUseCase = ObserveConversationsFromFolderUseCaseImpl( - conversationFolderRepository + conversationFolderRepository, + callRepository ) + init { + everySuspend { + callRepository.ongoingCallsFlow() + } returns flowOf(emptyList()) + } + suspend fun withConversationsFromFolder(folderId: String, conversationList: List) = apply { everySuspend { conversationFolderRepository.observeConversationsFromFolder(folderId) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt index 7576bde0dff7..f1f810fa34a4 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt @@ -141,7 +141,6 @@ internal object TestConversation { id = ENTITY_ID, name = "GROUP Name", type = ConversationEntity.Type.GROUP, - callStatus = null, previewAssetId = null, mutedStatus = ConversationEntity.MutedStatus.ALL_ALLOWED, teamId = TestTeam.TEAM_ID.value, @@ -326,7 +325,6 @@ internal object TestConversation { id = ENTITY_ID, name = "convo name", type = ConversationEntity.Type.SELF, - callStatus = null, previewAssetId = null, mutedStatus = ConversationEntity.MutedStatus.ALL_ALLOWED, teamId = "teamId", From 3a25dc4a9b6fa26c54bc330d8263c48848ac6419 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 17:01:58 +0200 Subject: [PATCH 2/3] feat: add functionality to observe active call conversation IDs and update related data structures --- .../logic/data/conversation/Conversation.kt | 3 - .../dao/conversation/ConversationDAOImpl.kt | 2 +- .../conversation/ConversationExtensions.kt | 2 +- .../kalium/logic/data/call/CallRepository.kt | 12 ++++ .../data/conversation/ConversationMapper.kt | 2 - .../ConversationRepositoryExtensions.kt | 6 +- .../kalium/logic/feature/call/CallsScope.kt | 7 +++ ...ObserveActiveCallConversationIdsUseCase.kt | 35 ++++++++++++ .../ActiveCallConversationMapper.kt | 41 -------------- .../feature/conversation/ConversationScope.kt | 5 +- ...onDetailsWithEventsBySearchQueryUseCase.kt | 12 +--- ...rsationListDetailsWithEventsUseCaseImpl.kt | 23 +------- .../ObserveConversationsFromFolderUseCase.kt | 20 +------ .../logic/data/call/CallRepositoryTest.kt | 1 - .../ConversationRepositoryExtensionsTest.kt | 2 +- .../EndCallOnConversationChangeUseCaseTest.kt | 1 - ...serveConversationListDetailsUseCaseTest.kt | 56 ------------------- ...serveConversationsFromFolderUseCaseTest.kt | 11 +--- 18 files changed, 68 insertions(+), 173 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveActiveCallConversationIdsUseCase.kt delete mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ActiveCallConversationMapper.kt diff --git a/core/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt b/core/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt index 70c91d2e4bd6..8ff6e9d1a705 100644 --- a/core/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt +++ b/core/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt @@ -328,7 +328,6 @@ sealed interface ConversationDetails { ) : ConversationDetails sealed interface Group : ConversationDetails { - val hasOngoingCall: Boolean val isSelfUserMember: Boolean val selfRole: Conversation.Member.Role? val isFavorite: Boolean @@ -338,7 +337,6 @@ sealed interface ConversationDetails { data class Regular( override val conversation: Conversation, - override val hasOngoingCall: Boolean = false, override val isSelfUserMember: Boolean, override val selfRole: Conversation.Member.Role?, override val isFavorite: Boolean = false, @@ -349,7 +347,6 @@ sealed interface ConversationDetails { data class Channel( override val conversation: Conversation, - override val hasOngoingCall: Boolean = false, override val isSelfUserMember: Boolean, override val selfRole: Conversation.Member.Role?, override val isFavorite: Boolean = false, diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index 470b53530731..c75e14fd0dce 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -348,7 +348,7 @@ internal class ConversationDAOImpl internal constructor( fromArchive = fromArchive, onlyInteractionsEnabled = onlyInteractionEnabled, newActivitiesOnTop = newActivitiesOnTop, - activeCallConversationIds = emptyList(), + activeCallConversationIds = emptySet(), strict_mls = if (strictMLSFilter) 1 else 0, mapper = conversationDetailsWithEventsMapper::fromViewToModel ).asFlow() diff --git a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt index 3b69ac52d5bc..f1468ec684c0 100644 --- a/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt +++ b/data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt @@ -38,7 +38,7 @@ interface ConversationExtensions { val fromArchive: Boolean = false, val onlyInteractionEnabled: Boolean = false, val newActivitiesOnTop: Boolean = false, - val activeCallConversationIds: List = emptyList(), + val activeCallConversationIds: Set = emptySet(), val conversationFilter: ConversationFilterEntity = ConversationFilterEntity.ALL, val strictMlsFilter: Boolean = true, ) 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 9c5059690f36..2d416e7e997c 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 @@ -85,6 +85,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flattenConcat @@ -110,6 +111,7 @@ internal interface CallRepository { suspend fun incomingCallsFlow(): Flow> suspend fun outgoingCallsFlow(): Flow> suspend fun ongoingCallsFlow(): Flow> + fun observeActiveCallConversationIds(): Flow> suspend fun establishedCallsFlow(): Flow> suspend fun establishedCallConversationId(): ConversationId? fun observeLastActiveCallByConversationId(conversationId: ConversationId): Flow @@ -231,6 +233,16 @@ internal class CallDataSource( .map { calls -> calls.filter { it.status == CallEntity.Status.STILL_ONGOING } } .combineWithCallsMetadata() + override fun observeActiveCallConversationIds(): Flow> = + activeCallEntities + .map { activeCalls -> + activeCalls + .filter { it.status.isActive } + .map { it.conversationId.toModel() } + .toSet() + } + .distinctUntilChanged() + override suspend fun establishedCallsFlow(): Flow> = activeCallEntities .map { calls -> calls.filter { it.status == CallEntity.Status.ESTABLISHED || it.status == CallEntity.Status.ANSWERED } } 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 f113a54716da..0e9647a4c5bc 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 @@ -294,7 +294,6 @@ internal class ConversationMapperImpl( if (isChannel) { ConversationDetails.Group.Channel( conversation = fromConversationViewToEntity(daoModel), - hasOngoingCall = false, isSelfUserMember = isMember, selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }, isFavorite = isFavorite, @@ -310,7 +309,6 @@ internal class ConversationMapperImpl( } else { ConversationDetails.Group.Regular( conversation = fromConversationViewToEntity(daoModel), - hasOngoingCall = false, isSelfUserMember = isMember, selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }, isFavorite = isFavorite, 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 e0f07958be86..3ea69ba62e79 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 @@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.map internal interface ConversationRepositoryExtensions { suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( queryConfig: ConversationQueryConfig, - activeCallConversationIds: List, + activeCallConversationIds: Set, pagingConfig: PagingConfig, startingOffset: Long, strictMlsFilter: Boolean, @@ -45,7 +45,7 @@ internal class ConversationRepositoryExtensionsImpl internal constructor( ) : ConversationRepositoryExtensions { override suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( queryConfig: ConversationQueryConfig, - activeCallConversationIds: List, + activeCallConversationIds: Set, pagingConfig: PagingConfig, startingOffset: Long, strictMlsFilter: Boolean, @@ -57,7 +57,7 @@ internal class ConversationRepositoryExtensionsImpl internal constructor( fromArchive = fromArchive, onlyInteractionEnabled = onlyInteractionEnabled, newActivitiesOnTop = newActivitiesOnTop, - activeCallConversationIds = activeCallConversationIds.map { it.toDao() }, + activeCallConversationIds = activeCallConversationIds.mapTo(mutableSetOf()) { it.toDao() }, conversationFilter = conversationFilter.toDao(), strictMlsFilter = strictMlsFilter ), 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 060fc453441f..209cbce02086 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 @@ -52,6 +52,8 @@ 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.ObserveActiveCallConversationIdsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveActiveCallConversationIdsUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveCallModerationActionsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveCallModerationActionsUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveCallQualityDataUseCase @@ -158,6 +160,11 @@ public class CallsScope internal constructor( callRepository = callRepository, ) + public val observeActiveCallConversationIds: ObserveActiveCallConversationIdsUseCase + get() = ObserveActiveCallConversationIdsUseCaseImpl( + callRepository = callRepository, + ) + internal val observeOngoingAndIncomingCalls: ObserveOngoingAndIncomingCallsUseCase by lazy { ObserveOngoingAndIncomingCallsUseCaseImpl(callRepository = callRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveActiveCallConversationIdsUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveActiveCallConversationIdsUseCase.kt new file mode 100644 index 000000000000..8323ce26893c --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveActiveCallConversationIdsUseCase.kt @@ -0,0 +1,35 @@ +/* + * Wire + * Copyright (C) 2026 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.id.ConversationId +import kotlinx.coroutines.flow.Flow + +public fun interface ObserveActiveCallConversationIdsUseCase { + public suspend operator fun invoke(): Flow> +} + +internal class ObserveActiveCallConversationIdsUseCaseImpl( + private val callRepository: CallRepository +) : ObserveActiveCallConversationIdsUseCase { + + override suspend fun invoke(): Flow> = + callRepository.observeActiveCallConversationIds() +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ActiveCallConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ActiveCallConversationMapper.kt deleted file mode 100644 index 0b6ab69e604f..000000000000 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ActiveCallConversationMapper.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Wire - * Copyright (C) 2026 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.data.conversation.ConversationDetails -import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents -import com.wire.kalium.logic.data.id.ConversationId - -internal fun ConversationDetailsWithEvents.withActiveCallStatus(activeCallConversationIds: Set): ConversationDetailsWithEvents { - val hasOngoingCall = conversationDetails.conversation.id in activeCallConversationIds - return copy( - conversationDetails = conversationDetails.withActiveCallStatus(hasOngoingCall), - hasNewActivitiesToShow = hasNewActivitiesToShow || hasOngoingCall - ) -} - -private fun ConversationDetails.withActiveCallStatus(hasOngoingCall: Boolean): ConversationDetails = - when (this) { - is ConversationDetails.Group.Regular -> copy(hasOngoingCall = hasOngoingCall) - is ConversationDetails.Group.Channel -> copy(hasOngoingCall = hasOngoingCall) - is ConversationDetails.Connection, - is ConversationDetails.OneOne, - is ConversationDetails.Self, - is ConversationDetails.Team -> this - } 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 e4aa1ad330a9..9c70faade160 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 @@ -170,8 +170,7 @@ public class ConversationScope internal constructor( get() = ObserveConversationListDetailsWithEventsUseCaseImpl( conversationRepository, conversationFolderRepository, - getFavoriteFolder, - callRepository + getFavoriteFolder ) public val observeConversationMembers: ObserveConversationMembersUseCase @@ -456,7 +455,7 @@ public class ConversationScope internal constructor( public val syncConversationCode: SyncConversationCodeUseCase get() = SyncConversationCodeUseCase(conversationGroupRepository, serverConfigLinks) public val observeConversationsFromFolder: ObserveConversationsFromFolderUseCase - get() = ObserveConversationsFromFolderUseCaseImpl(conversationFolderRepository, callRepository) + get() = ObserveConversationsFromFolderUseCaseImpl(conversationFolderRepository) public val getFavoriteFolder: GetFavoriteFolderUseCase get() = GetFavoriteFolderUseCaseImpl(conversationFolderRepository) public val addConversationToFavorites: AddConversationToFavoritesUseCase diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt index dd96d2f5e61f..83840ae5378f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt @@ -19,7 +19,6 @@ package com.wire.kalium.logic.feature.conversation import androidx.paging.PagingConfig import androidx.paging.PagingData -import androidx.paging.map import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationRepository @@ -30,7 +29,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart /** @@ -51,24 +49,18 @@ public class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase startingOffset: Long, strictMlsFilter: Boolean ): Flow> = - callRepository.ongoingCallsFlow() - .map { calls -> calls.map { it.conversationId }.toSet() } + callRepository.observeActiveCallConversationIds() .onStart { emit(emptySet()) } .distinctUntilChanged() .flatMapLatest { activeCallConversationIds -> conversationRepository.extensions .getPaginatedConversationDetailsWithEventsBySearchQuery( queryConfig = queryConfig, - activeCallConversationIds = activeCallConversationIds.toList(), + activeCallConversationIds = activeCallConversationIds, pagingConfig = pagingConfig, startingOffset = startingOffset, strictMlsFilter = strictMlsFilter ) - .map { pagingData -> - pagingData.map { conversation -> - conversation.withActiveCallStatus(activeCallConversationIds) - } - } } .flowOn(dispatcher.io) } 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 611e06cbba02..f57c84cc57bb 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 @@ -18,20 +18,14 @@ package com.wire.kalium.logic.feature.conversation -import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationFilter import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart /** * This use case will observe and return the list of conversation details for the current user. @@ -49,16 +43,14 @@ internal class ObserveConversationListDetailsWithEventsUseCaseImpl( private val conversationRepository: ConversationRepository, private val conversationFolderRepository: ConversationFolderRepository, private val getFavoriteFolder: GetFavoriteFolderUseCase, - private val callRepository: CallRepository ) : ObserveConversationListDetailsWithEventsUseCase { - @OptIn(ExperimentalCoroutinesApi::class) override suspend operator fun invoke( fromArchive: Boolean, conversationFilter: ConversationFilter, strictMlsFilter: Boolean, - ): Flow> { - val conversationsFlow = when (conversationFilter) { + ): Flow> = + when (conversationFilter) { ConversationFilter.Favorites -> when (val result = getFavoriteFolder()) { GetFavoriteFolderUseCase.Result.Failure -> flowOf(emptyList()) is GetFavoriteFolderUseCase.Result.Success -> @@ -74,15 +66,4 @@ internal class ObserveConversationListDetailsWithEventsUseCaseImpl( ConversationFilter.OneOnOne -> conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter, strictMlsFilter) } - - return callRepository.ongoingCallsFlow() - .map { calls -> calls.map { it.conversationId }.toSet() } - .onStart { emit(emptySet()) } - .distinctUntilChanged() - .flatMapLatest { activeCallConversationIds -> - conversationsFlow.map { conversations -> - conversations.map { it.withActiveCallStatus(activeCallConversationIds) } - } - } - } } 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 7ce415bb7527..bc0994883115 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 @@ -17,19 +17,12 @@ */ package com.wire.kalium.logic.feature.conversation.folder -import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository -import com.wire.kalium.logic.feature.conversation.withActiveCallStatus import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart /** * This use case will observe and return the list of conversations from given folder. @@ -41,21 +34,10 @@ public fun interface ObserveConversationsFromFolderUseCase { internal class ObserveConversationsFromFolderUseCaseImpl( private val conversationFolderRepository: ConversationFolderRepository, - private val callRepository: CallRepository, private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : ObserveConversationsFromFolderUseCase { - @OptIn(ExperimentalCoroutinesApi::class) override suspend operator fun invoke(folderId: String): Flow> = - callRepository.ongoingCallsFlow() - .map { calls -> calls.map { it.conversationId }.toSet() } - .onStart { emit(emptySet()) } - .distinctUntilChanged() - .flatMapLatest { activeCallConversationIds -> - conversationFolderRepository.observeConversationsFromFolder(folderId) - .map { conversations -> - conversations.map { it.withActiveCallStatus(activeCallConversationIds) } - } - } + conversationFolderRepository.observeConversationsFromFolder(folderId) .flowOn(dispatchers.io) } 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 6352b26f80bc..6eaac8bb0786 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 @@ -219,7 +219,6 @@ class CallRepositoryTest { Either.Right( ConversationDetails.Group.Regular( Arrangement.groupConversation, - false, isSelfUserMember = true, selfRole = Conversation.Member.Role.Member ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt index 1d9216d54e45..a8cf265b84f0 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt @@ -67,7 +67,7 @@ class ConversationRepositoryExtensionsTest { onlyInteractionEnabled = false, newActivitiesOnTop = false, ), - activeCallConversationIds = emptyList(), + activeCallConversationIds = emptySet(), pagingConfig = pagingConfig, startingOffset = 0L, strictMlsFilter = false diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt index 171f2bffa119..2dd8e3a66dd4 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt @@ -253,7 +253,6 @@ class EndCallOnConversationChangeUseCaseTest { private val groupConversationDetail = ConversationDetails.Group.Regular( conversation = conversation, - hasOngoingCall = true, isSelfUserMember = false, selfRole = null ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt index dabfed66fe72..95603afc14c5 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt @@ -244,62 +244,6 @@ class ObserveConversationListDetailsUseCaseTest { } } - @Suppress("FunctionNaming") - @Test - fun givenAnOngoingCall_whenFetchingConversationDetails_thenTheConversationShouldHaveAnOngoingCall() = runTest { - // Given - val groupConversation = TestConversation.GROUP() - val fetchArchivedConversations = false - val groupConversationDetails = ConversationDetails.Group.Regular( - groupConversation, - isSelfUserMember = true, - selfRole = Conversation.Member.Role.Member - ) - - val firstConversationsList = listOf(groupConversation) - - val conversationListUpdates = Channel>(Channel.UNLIMITED) - conversationListUpdates.send(firstConversationsList) - - val (_, observeConversationsUseCase) = Arrangement() - .withConversationsList(conversationListUpdates) - .withSuccessfulConversationsDetailsListUpdates(groupConversation, listOf(groupConversationDetails)) - .arrange() - - // When, Then - observeConversationsUseCase(fetchArchivedConversations).test { - assertEquals(true, (awaitItem()[0] as ConversationDetails.Group).hasOngoingCall) - } - } - - @Test - fun givenAConversationWithoutAnOngoingCall_whenFetchingConversationDetails_thenTheConversationShouldNotHaveAnOngoingCall() = runTest { - // Given - val groupConversation = TestConversation.GROUP() - val fetchArchivedConversations = false - - val groupConversationDetails = ConversationDetails.Group.Regular( - groupConversation, - isSelfUserMember = true, - selfRole = Conversation.Member.Role.Member - ) - - val firstConversationsList = listOf(groupConversation) - - val conversationListUpdates = Channel>(Channel.UNLIMITED) - conversationListUpdates.send(firstConversationsList) - - val (_, observeConversationsUseCase) = Arrangement() - .withConversationsList(conversationListUpdates) - .withSuccessfulConversationsDetailsListUpdates(groupConversation, listOf(groupConversationDetails)) - .arrange() - - // When, Then - observeConversationsUseCase(fetchArchivedConversations).test { - assertEquals(false, (awaitItem()[0] as ConversationDetails.Group).hasOngoingCall) - } - } - @Suppress("FunctionNaming") @Test fun givenConversationDetailsFailure_whenObservingDetailsList_thenIgnoreConversationWithFailure() = runTest { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt index ba5de6e24df5..a7acfb430fd9 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt @@ -17,7 +17,6 @@ */ package com.wire.kalium.logic.feature.conversation.folder -import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.framework.TestConversationDetails @@ -80,19 +79,11 @@ class ObserveConversationsFromFolderUseCaseTest { private class Arrangement { val conversationFolderRepository = mock(mode = MockMode.autoUnit) - val callRepository = mock() private val observeConversationsFromFolderUseCase = ObserveConversationsFromFolderUseCaseImpl( - conversationFolderRepository, - callRepository + conversationFolderRepository ) - init { - everySuspend { - callRepository.ongoingCallsFlow() - } returns flowOf(emptyList()) - } - suspend fun withConversationsFromFolder(folderId: String, conversationList: List) = apply { everySuspend { conversationFolderRepository.observeConversationsFromFolder(folderId) From b22345ef77d0659018dba0cca8e69eeee6f7f3f4 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 17:06:38 +0200 Subject: [PATCH 3/3] detekt --- .../kalium/logic/data/call/CallRepository.kt | 37 ++++++++++--------- ...ObserveActiveCallConversationIdsUseCase.kt | 3 ++ 2 files changed, 23 insertions(+), 17 deletions(-) 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 2d416e7e997c..7b81c8574a8a 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 @@ -425,23 +425,25 @@ internal class CallDataSource( conversationId.toLogString() ) val qualifiedIDEntity = callMapper.fromConversationIdToQualifiedIDEntity(conversationId = conversationId) - (_callMetadataProfile.value[conversationId]?.callerId?.toString() ?: callDAO.getCallerIdByConversationId( - conversationId = qualifiedIDEntity - ))?.let { callerId -> - val qualifiedUserId = qualifiedIdMapper.fromStringToQualifiedID(callerId) - - val message = Message.System( - Uuid.random().toString(), - MessageContent.MissedCall, - conversationId, - Clock.System.now(), - qualifiedUserId, - Message.Status.Sent, - Message.Visibility.VISIBLE, - expirationData = null, - ) - persistMessage(message) - } ?: callingLogger.i("[CallRepository] -> Unable to persist Missed Call due to missing Caller ID") + ( + _callMetadataProfile.value[conversationId]?.callerId?.toString() ?: callDAO.getCallerIdByConversationId( + conversationId = qualifiedIDEntity + ) + )?.let { callerId -> + val qualifiedUserId = qualifiedIdMapper.fromStringToQualifiedID(callerId) + + val message = Message.System( + Uuid.random().toString(), + MessageContent.MissedCall, + conversationId, + Clock.System.now(), + qualifiedUserId, + Message.Status.Sent, + Message.Visibility.VISIBLE, + expirationData = null, + ) + persistMessage(message) + } ?: callingLogger.i("[CallRepository] -> Unable to persist Missed Call due to missing Caller ID") } override fun updateIsMutedById(conversationId: ConversationId, isMuted: Boolean) { @@ -612,6 +614,7 @@ internal class CallDataSource( staleOpenCallsCleanupFinishedFlow.value = true callingLogger.i("Stale open calls cleanup is done") } + override fun observeStaleOpenCallsCleanupFinished(): Flow = staleOpenCallsCleanupFinishedFlow override suspend fun establishedCallConversationId(): ConversationId? = diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveActiveCallConversationIdsUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveActiveCallConversationIdsUseCase.kt index 8323ce26893c..862cdfbb7e2a 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveActiveCallConversationIdsUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveActiveCallConversationIdsUseCase.kt @@ -22,6 +22,9 @@ import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.id.ConversationId import kotlinx.coroutines.flow.Flow +/** + * Use case to observe the conversation ids of active calls. This is used to determine if a conversation has an active call. + */ public fun interface ObserveActiveCallConversationIdsUseCase { public suspend operator fun invoke(): Flow> }