From cebfc41e486893015f01788d5849748cc1a1bdeb Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:55:27 +0200 Subject: [PATCH 1/5] refactor: enhance conversation details with ongoing call status and improve query logic --- .../main/kotlin/com/wire/android/mapper/ConversationMapper.kt | 4 ++-- .../composer/MessageComposerViewModelArrangement.kt | 1 - .../home/conversations/details/GroupDetailsViewModelTest.kt | 2 -- .../details/updateappsaccess/UpdateAppsAccessViewModelTest.kt | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt index 067b649e6ab..d94f81739c5 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt @@ -60,7 +60,7 @@ fun ConversationDetailsWithEvents.toConversationItem( mutedStatus = conversationDetails.conversation.mutedStatus, unreadEventCount = unreadEventCount ), - hasOnGoingCall = conversationDetails.hasOngoingCall && conversationDetails.isSelfUserMember, + hasOnGoingCall = hasOngoingCall && conversationDetails.isSelfUserMember, isFromTheSameTeam = conversationDetails.conversation.teamId == selfUserTeamId, isSelfUserMember = conversationDetails.isSelfUserMember, teamId = conversationDetails.conversation.teamId, @@ -85,7 +85,7 @@ fun ConversationDetailsWithEvents.toConversationItem( mutedStatus = conversationDetails.conversation.mutedStatus, unreadEventCount = unreadEventCount ), - hasOnGoingCall = conversationDetails.hasOngoingCall && conversationDetails.isSelfUserMember, + hasOnGoingCall = hasOngoingCall && conversationDetails.isSelfUserMember, isFromTheSameTeam = conversationDetails.conversation.teamId == selfUserTeamId, isSelfUserMember = conversationDetails.isSelfUserMember, teamId = conversationDetails.conversation.teamId, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt index 56374c912f2..bdb2166d84c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt @@ -242,7 +242,6 @@ internal fun mockConversationDetailsGroup( ) = ConversationDetails.Group.Regular( conversation = TestConversation.GROUP() .copy(name = conversationName, id = mockedConversationId, teamId = teamId), - hasOngoingCall = false, isSelfUserMember = true, selfRole = Conversation.Member.Role.Member, wireCell = wireCell, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupDetailsViewModelTest.kt index 87af658115b..1bf7c4be696 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupDetailsViewModelTest.kt @@ -699,7 +699,6 @@ class GroupDetailsViewModelTest { proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, legalHoldStatus = Conversation.LegalHoldStatus.ENABLED ), - hasOngoingCall = false, isSelfUserMember = true, selfRole = Conversation.Member.Role.Member, wireCell = null, @@ -729,7 +728,6 @@ class GroupDetailsViewModelTest { proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, legalHoldStatus = Conversation.LegalHoldStatus.ENABLED ), - hasOngoingCall = false, isSelfUserMember = true, selfRole = Conversation.Member.Role.Member, wireCell = null, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModelTest.kt index ed5e0190b98..9370089857a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModelTest.kt @@ -385,7 +385,6 @@ class UpdateAppsAccessViewModelTest { proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED, legalHoldStatus = Conversation.LegalHoldStatus.ENABLED ), - hasOngoingCall = false, isSelfUserMember = true, selfRole = Conversation.Member.Role.Member, wireCell = null, From 99bf3a21fa0933e7c6fc7b387d53c7f44f3be7fd Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Wed, 1 Jul 2026 00:57:17 +0200 Subject: [PATCH 2/5] feat: add joinableCallsFlow to retrieve calls that can be joined --- .../com/wire/android/di/accountScoped/CallsModule.kt | 5 +++++ .../wire/android/ui/calling/CallingViewModelFactory.kt | 6 +++--- .../conversations/call/ConversationCallViewModel.kt | 6 +++--- .../call/ConversationCallViewModelTest.kt | 10 +++++----- kalium | 2 +- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt index 1427966dd85..d05fd478ccd 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt @@ -37,6 +37,7 @@ import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnable import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallWithSortedParticipantsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveInCallReactionsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveJoinableCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveLastActiveCallWithSortedParticipantsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase @@ -83,6 +84,10 @@ class CallsModule { fun provideObserveOngoingCallsUseCase(callsScope: CallsScope): ObserveOngoingCallsUseCase = callsScope.observeOngoingCalls + @Provides + fun provideObserveJoinableCallsUseCase(callsScope: CallsScope): ObserveJoinableCallsUseCase = + callsScope.observeJoinableCalls + @Provides fun provideObserveEstablishedCallWithSortedParticipantsUseCase( callsScope: CallsScope diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt index e65afc49129..09567bd9b73 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallingViewModelFactory.kt @@ -57,8 +57,8 @@ import com.wire.kalium.logic.feature.call.usecase.ObserveCallQualityDataUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveInCallReactionsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveJoinableCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveLastActiveCallWithSortedParticipantsUseCase -import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveSpeakerUseCase import com.wire.kalium.logic.feature.call.usecase.RejectCallUseCase @@ -132,7 +132,7 @@ class CallingViewModelFactory @Inject constructor( private val flipToBackCamera: FlipToBackCameraUseCase, private val observeSpeaker: ObserveSpeakerUseCase, private val userTypeMapper: UserTypeMapper, - private val observeOngoingCalls: ObserveOngoingCallsUseCase, + private val observeJoinableCalls: ObserveJoinableCallsUseCase, private val observeParticipantsForConversation: ObserveParticipantsForConversationUseCase, private val observeSyncState: ObserveSyncStateUseCase, private val isConferenceCallingEnabled: IsEligibleToStartCallUseCase, @@ -219,7 +219,7 @@ class CallingViewModelFactory @Inject constructor( fun conversationCallViewModel(savedStateHandle: SavedStateHandle) = ConversationCallViewModel( savedStateHandle = savedStateHandle, currentAccount = currentAccount, - observeOngoingCalls = observeOngoingCalls, + observeJoinableCalls = observeJoinableCalls, observeEstablishedCalls = observeEstablishedCalls, observeParticipantsForConversation = observeParticipantsForConversation, answerCall = answerCall, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt index 8b831ddb156..cb096e604a6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt @@ -36,7 +36,7 @@ import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase -import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveJoinableCallsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase @@ -51,7 +51,7 @@ import kotlinx.coroutines.launch class ConversationCallViewModel( val savedStateHandle: SavedStateHandle, currentAccount: UserId, - private val observeOngoingCalls: ObserveOngoingCallsUseCase, + private val observeJoinableCalls: ObserveJoinableCallsUseCase, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val observeParticipantsForConversation: ObserveParticipantsForConversationUseCase, private val answerCall: AnswerCallUseCase, @@ -107,7 +107,7 @@ class ConversationCallViewModel( } private fun listenOngoingCall() = viewModelScope.launch { - combine(observeOngoingCalls(), observeConversationDetails(conversationId)) { calls, conversationDetailsResult -> + combine(observeJoinableCalls(), observeConversationDetails(conversationId)) { calls, conversationDetailsResult -> val hasOngoingCall = calls.any { call -> call.conversationId == conversationId } // valid conversation is a conversation where the user is a member and it's not deleted val validConversation = when (conversationDetailsResult) { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelTest.kt index b6a36b3fe35..377264ad786 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelTest.kt @@ -40,7 +40,7 @@ import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase -import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveJoinableCallsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase @@ -156,7 +156,7 @@ class ConversationCallViewModelTest { private lateinit var savedStateHandle: SavedStateHandle @MockK - private lateinit var observeOngoingCalls: ObserveOngoingCallsUseCase + private lateinit var observeJoinableCalls: ObserveJoinableCallsUseCase @MockK private lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase @@ -198,7 +198,7 @@ class ConversationCallViewModelTest { MockKAnnotations.init(this, relaxUnitFun = true) every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId = conversationId) coEvery { observeEstablishedCalls.invoke() } returns emptyFlow() - coEvery { observeOngoingCalls.invoke() } returns emptyFlow() + coEvery { observeJoinableCalls.invoke() } returns emptyFlow() coEvery { observeConversationDetails(any()) } returns flowOf() coEvery { observeParticipantsForConversation(any()) } returns flowOf() every { observeSyncState() } returns flowOf(SyncState.Live) @@ -220,7 +220,7 @@ class ConversationCallViewModelTest { } fun withOngoingCalls(calls: List) = apply { - coEvery { observeOngoingCalls() } returns flowOf(calls) + coEvery { observeJoinableCalls() } returns flowOf(calls) } fun withConversationDetails(result: ObserveConversationDetailsUseCase.Result) = apply { @@ -229,7 +229,7 @@ class ConversationCallViewModelTest { fun arrange(): Pair = this to ConversationCallViewModel( savedStateHandle = savedStateHandle, - observeOngoingCalls = observeOngoingCalls, + observeJoinableCalls = observeJoinableCalls, observeEstablishedCalls = observeEstablishedCalls, answerCall = joinCall, endCall = endCall, diff --git a/kalium b/kalium index 22bf2fea615..48edf7f7b2b 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 22bf2fea6152bf87d5b25ceb2b5ae0acf54b4e89 +Subproject commit 48edf7f7b2b2283ef8e257fa42f8f1e1d2b401b2 From 9ab4a6cd07db95ff1c42d36e99133883f7cf1e8d Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:25:21 +0200 Subject: [PATCH 3/5] refactor: remove hasOngoingCall from the Conversation data class --- .../wire/android/mapper/ConversationMapper.kt | 17 ++++-- .../android/ui/home/HomeViewModelFactory.kt | 3 + .../GetConversationsFromSearchUseCase.kt | 22 ++++++-- .../ConversationListViewModel.kt | 9 ++- .../GetConversationsFromSearchUseCaseTest.kt | 56 ++++++++++++++++++- .../ConversationListViewModelTest.kt | 6 ++ 6 files changed, 99 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt index d94f81739c5..24136d5bef3 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt @@ -38,6 +38,7 @@ import com.wire.kalium.logic.data.conversation.ConversationDetails.Self import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.conversation.UnreadEventCount +import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.message.UnreadEventType import com.wire.kalium.logic.data.user.ConnectionState @@ -47,9 +48,11 @@ import com.wire.kalium.logic.data.user.UserAvailabilityStatus fun ConversationDetailsWithEvents.toConversationItem( userTypeMapper: UserTypeMapper, uiTextResolver: UiTextResolver, - selfUserTeamId: TeamId? + selfUserTeamId: TeamId?, + joinableCallConversationIds: Set = emptySet() ): ConversationItem = when (val conversationDetails = this.conversationDetails) { is Group.Regular -> { + val hasJoinableCall = conversationDetails.hasJoinableCall(joinableCallConversationIds) Regular( groupName = conversationDetails.conversation.name.orEmpty(), conversationId = conversationDetails.conversation.id, @@ -60,7 +63,7 @@ fun ConversationDetailsWithEvents.toConversationItem( mutedStatus = conversationDetails.conversation.mutedStatus, unreadEventCount = unreadEventCount ), - hasOnGoingCall = hasOngoingCall && conversationDetails.isSelfUserMember, + hasOnGoingCall = hasJoinableCall, isFromTheSameTeam = conversationDetails.conversation.teamId == selfUserTeamId, isSelfUserMember = conversationDetails.isSelfUserMember, teamId = conversationDetails.conversation.teamId, @@ -68,13 +71,14 @@ fun ConversationDetailsWithEvents.toConversationItem( isArchived = conversationDetails.conversation.archived, mlsVerificationStatus = conversationDetails.conversation.mlsVerificationStatus, proteusVerificationStatus = conversationDetails.conversation.proteusVerificationStatus, - hasNewActivitiesToShow = hasNewActivitiesToShow, + hasNewActivitiesToShow = hasNewActivitiesToShow || hasJoinableCall, isFavorite = conversationDetails.isFavorite, folder = conversationDetails.folder ) } is Group.Channel -> { + val hasJoinableCall = conversationDetails.hasJoinableCall(joinableCallConversationIds) Channel( groupName = conversationDetails.conversation.name.orEmpty(), conversationId = conversationDetails.conversation.id, @@ -85,7 +89,7 @@ fun ConversationDetailsWithEvents.toConversationItem( mutedStatus = conversationDetails.conversation.mutedStatus, unreadEventCount = unreadEventCount ), - hasOnGoingCall = hasOngoingCall && conversationDetails.isSelfUserMember, + hasOnGoingCall = hasJoinableCall, isFromTheSameTeam = conversationDetails.conversation.teamId == selfUserTeamId, isSelfUserMember = conversationDetails.isSelfUserMember, teamId = conversationDetails.conversation.teamId, @@ -93,7 +97,7 @@ fun ConversationDetailsWithEvents.toConversationItem( isArchived = conversationDetails.conversation.archived, mlsVerificationStatus = conversationDetails.conversation.mlsVerificationStatus, proteusVerificationStatus = conversationDetails.conversation.proteusVerificationStatus, - hasNewActivitiesToShow = hasNewActivitiesToShow, + hasNewActivitiesToShow = hasNewActivitiesToShow || hasJoinableCall, isFavorite = conversationDetails.isFavorite, folder = conversationDetails.folder, isPrivate = conversationDetails.access == Group.Channel.ChannelAccess.PRIVATE @@ -170,6 +174,9 @@ fun ConversationDetailsWithEvents.toConversationItem( } } +private fun Group.hasJoinableCall(joinableCallConversationIds: Set): Boolean = + conversation.id in joinableCallConversationIds && isSelfUserMember + private fun parseConnectionEventType(connectionState: ConnectionState) = if (connectionState == ConnectionState.SENT) { BadgeEventType.SentConnectRequest diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModelFactory.kt index acd1810f619..8d3039a482f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModelFactory.kt @@ -37,6 +37,7 @@ import com.wire.android.util.ui.UiTextResolver import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.channels.ObserveChannelsCreationPermissionUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveJoinableCallsUseCase import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase import com.wire.kalium.logic.feature.conversation.ObserveArchivedUnreadConversationsCountUseCase @@ -77,6 +78,7 @@ class HomeViewModelFactory @Inject constructor( private val disableAppLockUseCase: Lazy, private val getConversationsPaginated: GetConversationsFromSearchUseCase, private val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase, + private val observeJoinableCalls: ObserveJoinableCallsUseCase, private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, private val refreshConversationsWithoutMetadata: RefreshConversationsWithoutMetadataUseCase, private val observeLegalHoldStateForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, @@ -140,6 +142,7 @@ class HomeViewModelFactory @Inject constructor( userTypeMapper = userTypeMapper, getSelfTeamId = getSelfTeamId, uiTextResolver = uiTextResolver, + observeJoinableCalls = observeJoinableCalls, ) fun newConversationViewModel() = NewConversationViewModel( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt index ca5b1d1c4cc..8189ec3fb0f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt @@ -31,12 +31,14 @@ import com.wire.android.util.ui.UiTextResolver import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationFilter import com.wire.kalium.logic.data.conversation.ConversationQueryConfig +import com.wire.kalium.logic.feature.call.usecase.ObserveJoinableCallsUseCase import com.wire.kalium.logic.feature.conversation.GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCase import com.wire.kalium.logic.feature.user.GetSelfTeamIdUseCase import dev.zacsweers.metro.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -49,6 +51,7 @@ class GetConversationsFromSearchUseCase @Inject constructor( private val dispatchers: DispatcherProvider, private val getSelfTeamId: GetSelfTeamIdUseCase, private val uiTextResolver: UiTextResolver, + private val observeJoinableCalls: ObserveJoinableCallsUseCase, ) { @Suppress("LongParameterList") suspend operator fun invoke( @@ -66,7 +69,7 @@ class GetConversationsFromSearchUseCase @Inject constructor( initialLoadSize = INITIAL_LOAD_SIZE, enablePlaceholders = true, ) - return when (conversationFilter) { + val conversationDetailsFlow = when (conversationFilter) { ConversationFilter.All, ConversationFilter.Groups, ConversationFilter.Channels, @@ -96,11 +99,18 @@ class GetConversationsFromSearchUseCase @Inject constructor( observeConversationsFromFromFolder(conversationFilter.folderId) .map { staticPagingItems(it) } } - }.map { pagingData -> - pagingData.map { - it.toConversationItem(userTypeMapper, uiTextResolver, selfUserTeamId) - } - }.flowOn(dispatchers.io()) + } + return combine(conversationDetailsFlow, observeJoinableCalls()) { pagingData, joinableCalls -> + val joinableCallConversationIds = joinableCalls.map { it.conversationId }.toSet() + pagingData.map { + it.toConversationItem( + userTypeMapper = userTypeMapper, + uiTextResolver = uiTextResolver, + selfUserTeamId = selfUserTeamId, + joinableCallConversationIds = joinableCallConversationIds + ) + } + }.flowOn(dispatchers.io()) } private fun staticPagingItems(conversations: List): PagingData { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt index 3b99bfd1943..20127e97e82 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt @@ -52,6 +52,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationFilter import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.call.usecase.ObserveJoinableCallsUseCase import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsWithEventsUseCase import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase @@ -66,6 +67,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow @@ -111,6 +113,7 @@ class ConversationListViewModelImpl( private val userTypeMapper: UserTypeMapper, private val getSelfTeamId: GetSelfTeamIdUseCase, private val uiTextResolver: UiTextResolver, + private val observeJoinableCalls: ObserveJoinableCallsUseCase, ) : ConversationListViewModel, ViewModel() { private val _infoMessage = MutableSharedFlow() @@ -213,12 +216,14 @@ class ConversationListViewModelImpl( observeConversationListDetailsWithEvents( fromArchive = conversationsSource == ConversationsSource.ARCHIVE, conversationFilter = conversationsSource.toFilter() - ).map { conversations -> + ).combine(observeJoinableCalls()) { conversations, joinableCalls -> + val joinableCallConversationIds = joinableCalls.map { it.conversationId }.toSet() conversations.map { conversationDetails -> conversationDetails.toConversationItem( userTypeMapper = userTypeMapper, uiTextResolver = uiTextResolver, - selfUserTeamId = selfTeamId + selfUserTeamId = selfTeamId, + joinableCallConversationIds = joinableCallConversationIds ) } to searchQuery } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt index 90e0c05d5f8..4682a74de18 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt @@ -28,11 +28,16 @@ import com.wire.android.ui.home.conversationslist.model.ConversationItem import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.util.ui.UiTextResolver import com.wire.android.util.ui.UIText +import com.wire.kalium.logic.data.call.Call +import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationFilter import com.wire.kalium.logic.data.conversation.ConversationFolder import com.wire.kalium.logic.data.conversation.ConversationQueryConfig import com.wire.kalium.logic.data.conversation.FolderType +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.call.usecase.ObserveJoinableCallsUseCase import com.wire.kalium.logic.feature.conversation.GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCase @@ -186,6 +191,33 @@ class GetConversationsFromSearchUseCaseTest { assertEquals(false, conversation.isFromTheSameTeam) } + @Test + fun givenJoinableGroupCall_whenGettingPaginatedList_thenGroupItemHasOngoingCallMarker() = + runTest(dispatcherProvider.main()) { + // Given + val conversation = ConversationDetailsWithEvents(TestConversationDetails.GROUP) + val (arrangement, useCase) = Arrangement() + .withPaginatedResult(listOf(conversation)) + .withJoinableCalls(listOf(joinableCall(conversation.conversationDetails.conversation.id))) + .arrange() + + // When + val result = with(arrangement.queryConfig) { + useCase( + searchQuery = searchQuery, + fromArchive = fromArchive, + newActivitiesOnTop = newActivitiesOnTop, + onlyInteractionEnabled = onlyInteractionEnabled, + useStrictMlsFilter = true + ).asSnapshot() + } + + // Then + val group = assertInstanceOf(result.first()) + assertEquals(true, group.hasOnGoingCall) + assertEquals(true, group.hasNewActivitiesToShow) + } + inner class Arrangement { @MockK @@ -197,6 +229,9 @@ class GetConversationsFromSearchUseCaseTest { @MockK lateinit var observeConversationsFromFolderUseCase: ObserveConversationsFromFolderUseCase + @MockK + lateinit var observeJoinableCalls: ObserveJoinableCallsUseCase + @MockK lateinit var userTypeMapper: UserTypeMapper @@ -225,6 +260,7 @@ class GetConversationsFromSearchUseCaseTest { } } coEvery { uiTextResolver.localeTag() } returns "test-locale" + coEvery { observeJoinableCalls() } returns flowOf(emptyList()) withSelfTeamId() withPaginatedResult(emptyList()) } @@ -245,6 +281,10 @@ class GetConversationsFromSearchUseCaseTest { } returns flowOf(conversations) } + fun withJoinableCalls(calls: List) = apply { + coEvery { observeJoinableCalls() } returns flowOf(calls) + } + fun withSelfTeamId() = apply { coEvery { getSelfTeamId() } returns TestUser.SELF_USER.teamId } @@ -256,7 +296,21 @@ class GetConversationsFromSearchUseCaseTest { userTypeMapper, dispatcherProvider, getSelfTeamId, - uiTextResolver + uiTextResolver, + observeJoinableCalls ) } + + private fun joinableCall(conversationId: ConversationId) = Call( + conversationId = conversationId, + status = CallStatus.STILL_ONGOING, + isMuted = false, + isCameraOn = false, + isCbrEnabled = false, + callerId = TestUser.SELF_USER_ID, + conversationName = null, + conversationType = Conversation.Type.Group.Regular, + callerName = null, + callerTeamName = null + ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt index 0bf67c324fe..354f4e5bb2b 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt @@ -42,6 +42,7 @@ import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationFilter import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.call.usecase.ObserveJoinableCallsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsWithEventsUseCase import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser @@ -295,6 +296,9 @@ class ConversationListViewModelTest { private lateinit var observeConversationListDetailsWithEventsUseCase: ObserveConversationListDetailsWithEventsUseCase + @MockK + private lateinit var observeJoinableCalls: ObserveJoinableCallsUseCase + @MockK private lateinit var observeLegalHoldStateForSelfUserUseCase: ObserveLegalHoldStateForSelfUserUseCase @@ -323,6 +327,7 @@ class ConversationListViewModelTest { } ) every { audioMessagePlayer.playingAudioMessageFlow } returns flowOf(PlayingAudioMessage.None) + coEvery { observeJoinableCalls() } returns flowOf(emptyList()) coEvery { getSelfTeamId() } returns TestUser.SELF_USER.teamId coEvery { uiTextResolver.resolve(any()) } answers { val text = firstArg() @@ -371,6 +376,7 @@ class ConversationListViewModelTest { refreshConversationsWithoutMetadata = refreshConversationsWithoutMetadata, currentAccount = TestUser.SELF_USER_ID, observeConversationListDetailsWithEvents = observeConversationListDetailsWithEventsUseCase, + observeJoinableCalls = observeJoinableCalls, observeLegalHoldStateForSelfUser = observeLegalHoldStateForSelfUserUseCase, userTypeMapper = UserTypeMapper(), getSelfTeamId = getSelfTeamId, From 5dfe736440130f7e16f6484677a2cbe71d923f70 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:59:21 +0200 Subject: [PATCH 4/5] refactor: update joinableCallsFlow to return calls keyed by conversation ID --- .../com/wire/android/mapper/ConversationMapper.kt | 11 ++++++----- .../conversations/call/ConversationCallViewModel.kt | 4 ++-- .../usecase/GetConversationsFromSearchUseCase.kt | 5 ++--- .../conversationslist/ConversationListViewModel.kt | 5 ++--- .../call/ConversationCallViewModelTest.kt | 2 +- .../usecase/GetConversationsFromSearchUseCaseTest.kt | 4 ++-- .../ConversationListViewModelTest.kt | 2 +- 7 files changed, 16 insertions(+), 17 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt index 24136d5bef3..cb94792be07 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt @@ -30,6 +30,7 @@ import com.wire.android.ui.home.conversationslist.model.ConversationItem.Group.C import com.wire.android.ui.home.conversationslist.model.ConversationItem.Group.Regular import com.wire.android.ui.home.conversationslist.model.ConversationItem.PrivateConversation import com.wire.android.util.ui.UiTextResolver +import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationDetails.Connection import com.wire.kalium.logic.data.conversation.ConversationDetails.Group @@ -49,10 +50,10 @@ fun ConversationDetailsWithEvents.toConversationItem( userTypeMapper: UserTypeMapper, uiTextResolver: UiTextResolver, selfUserTeamId: TeamId?, - joinableCallConversationIds: Set = emptySet() + joinableCallsByConversationId: Map = emptyMap() ): ConversationItem = when (val conversationDetails = this.conversationDetails) { is Group.Regular -> { - val hasJoinableCall = conversationDetails.hasJoinableCall(joinableCallConversationIds) + val hasJoinableCall = conversationDetails.hasJoinableCall(joinableCallsByConversationId) Regular( groupName = conversationDetails.conversation.name.orEmpty(), conversationId = conversationDetails.conversation.id, @@ -78,7 +79,7 @@ fun ConversationDetailsWithEvents.toConversationItem( } is Group.Channel -> { - val hasJoinableCall = conversationDetails.hasJoinableCall(joinableCallConversationIds) + val hasJoinableCall = conversationDetails.hasJoinableCall(joinableCallsByConversationId) Channel( groupName = conversationDetails.conversation.name.orEmpty(), conversationId = conversationDetails.conversation.id, @@ -174,8 +175,8 @@ fun ConversationDetailsWithEvents.toConversationItem( } } -private fun Group.hasJoinableCall(joinableCallConversationIds: Set): Boolean = - conversation.id in joinableCallConversationIds && isSelfUserMember +private fun Group.hasJoinableCall(joinableCallsByConversationId: Map): Boolean = + joinableCallsByConversationId.containsKey(conversation.id) && isSelfUserMember private fun parseConnectionEventType(connectionState: ConnectionState) = if (connectionState == ConnectionState.SENT) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt index cb096e604a6..f657ddaa9a4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModel.kt @@ -107,8 +107,8 @@ class ConversationCallViewModel( } private fun listenOngoingCall() = viewModelScope.launch { - combine(observeJoinableCalls(), observeConversationDetails(conversationId)) { calls, conversationDetailsResult -> - val hasOngoingCall = calls.any { call -> call.conversationId == conversationId } + combine(observeJoinableCalls(), observeConversationDetails(conversationId)) { callsByConversationId, conversationDetailsResult -> + val hasOngoingCall = callsByConversationId.containsKey(conversationId) // valid conversation is a conversation where the user is a member and it's not deleted val validConversation = when (conversationDetailsResult) { is ObserveConversationDetailsUseCase.Result.Success -> { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt index 8189ec3fb0f..05eef74204a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt @@ -100,14 +100,13 @@ class GetConversationsFromSearchUseCase @Inject constructor( .map { staticPagingItems(it) } } } - return combine(conversationDetailsFlow, observeJoinableCalls()) { pagingData, joinableCalls -> - val joinableCallConversationIds = joinableCalls.map { it.conversationId }.toSet() + return combine(conversationDetailsFlow, observeJoinableCalls()) { pagingData, joinableCallsByConversationId -> pagingData.map { it.toConversationItem( userTypeMapper = userTypeMapper, uiTextResolver = uiTextResolver, selfUserTeamId = selfUserTeamId, - joinableCallConversationIds = joinableCallConversationIds + joinableCallsByConversationId = joinableCallsByConversationId ) } }.flowOn(dispatchers.io()) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt index 20127e97e82..43ae803d943 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt @@ -216,14 +216,13 @@ class ConversationListViewModelImpl( observeConversationListDetailsWithEvents( fromArchive = conversationsSource == ConversationsSource.ARCHIVE, conversationFilter = conversationsSource.toFilter() - ).combine(observeJoinableCalls()) { conversations, joinableCalls -> - val joinableCallConversationIds = joinableCalls.map { it.conversationId }.toSet() + ).combine(observeJoinableCalls()) { conversations, joinableCallsByConversationId -> conversations.map { conversationDetails -> conversationDetails.toConversationItem( userTypeMapper = userTypeMapper, uiTextResolver = uiTextResolver, selfUserTeamId = selfTeamId, - joinableCallConversationIds = joinableCallConversationIds + joinableCallsByConversationId = joinableCallsByConversationId ) } to searchQuery } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelTest.kt index 377264ad786..beb4c274ffc 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelTest.kt @@ -220,7 +220,7 @@ class ConversationCallViewModelTest { } fun withOngoingCalls(calls: List) = apply { - coEvery { observeJoinableCalls() } returns flowOf(calls) + coEvery { observeJoinableCalls() } returns flowOf(calls.associateBy { it.conversationId }) } fun withConversationDetails(result: ObserveConversationDetailsUseCase.Result) = apply { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt index 4682a74de18..dc79b3385a2 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt @@ -260,7 +260,7 @@ class GetConversationsFromSearchUseCaseTest { } } coEvery { uiTextResolver.localeTag() } returns "test-locale" - coEvery { observeJoinableCalls() } returns flowOf(emptyList()) + coEvery { observeJoinableCalls() } returns flowOf(emptyMap()) withSelfTeamId() withPaginatedResult(emptyList()) } @@ -282,7 +282,7 @@ class GetConversationsFromSearchUseCaseTest { } fun withJoinableCalls(calls: List) = apply { - coEvery { observeJoinableCalls() } returns flowOf(calls) + coEvery { observeJoinableCalls() } returns flowOf(calls.associateBy { it.conversationId }) } fun withSelfTeamId() = apply { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt index 354f4e5bb2b..85aab5e6cd9 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt @@ -327,7 +327,7 @@ class ConversationListViewModelTest { } ) every { audioMessagePlayer.playingAudioMessageFlow } returns flowOf(PlayingAudioMessage.None) - coEvery { observeJoinableCalls() } returns flowOf(emptyList()) + coEvery { observeJoinableCalls() } returns flowOf(emptyMap()) coEvery { getSelfTeamId() } returns TestUser.SELF_USER.teamId coEvery { uiTextResolver.resolve(any()) } answers { val text = firstArg() From 1e6574c14346f6b61680d2c7b7c1277c959ca32e Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:13:12 +0200 Subject: [PATCH 5/5] refactor: improve ongoing call handling in conversation details pagination --- .../GetConversationsFromSearchUseCase.kt | 125 ++++++++++++++---- .../GetConversationsFromSearchUseCaseTest.kt | 37 ++++++ 2 files changed, 137 insertions(+), 25 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt index 05eef74204a..f0d1b950343 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCase.kt @@ -28,9 +28,12 @@ import com.wire.android.mapper.toConversationItem import com.wire.android.ui.home.conversationslist.model.ConversationItem import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.UiTextResolver +import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents import com.wire.kalium.logic.data.conversation.ConversationFilter import com.wire.kalium.logic.data.conversation.ConversationQueryConfig +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.feature.call.usecase.ObserveJoinableCallsUseCase import com.wire.kalium.logic.feature.conversation.GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase @@ -38,10 +41,15 @@ import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFro import com.wire.kalium.logic.feature.user.GetSelfTeamIdUseCase import dev.zacsweers.metro.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch class GetConversationsFromSearchUseCase @Inject constructor( private val useCase: GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase, @@ -63,28 +71,52 @@ class GetConversationsFromSearchUseCase @Inject constructor( useStrictMlsFilter: Boolean, ): Flow> { val selfUserTeamId = getSelfTeamId() + val joinableCallsFlow = observeJoinableCalls() + val initialJoinableCallsByConversationId = joinableCallsFlow.first() val pagingConfig = PagingConfig( pageSize = PAGE_SIZE, prefetchDistance = PREFETCH_DISTANCE, initialLoadSize = INITIAL_LOAD_SIZE, enablePlaceholders = true, ) - val conversationDetailsFlow = when (conversationFilter) { + return when (conversationFilter) { ConversationFilter.All, ConversationFilter.Groups, - ConversationFilter.Channels, - ConversationFilter.OneOnOne -> useCase( - queryConfig = ConversationQueryConfig( - searchQuery = searchQuery, - fromArchive = fromArchive, - newActivitiesOnTop = newActivitiesOnTop, - onlyInteractionEnabled = onlyInteractionEnabled, - conversationFilter = conversationFilter, - ), - pagingConfig = pagingConfig, - startingOffset = 0L, - strictMlsFilter = useStrictMlsFilter - ) + ConversationFilter.Channels -> + useCase( + queryConfig = ConversationQueryConfig( + searchQuery = searchQuery, + fromArchive = fromArchive, + newActivitiesOnTop = newActivitiesOnTop, + onlyInteractionEnabled = onlyInteractionEnabled, + conversationFilter = conversationFilter, + ), + pagingConfig = pagingConfig, + startingOffset = 0L, + strictMlsFilter = useStrictMlsFilter + ).mapToConversationItems( + selfUserTeamId = selfUserTeamId, + initialJoinableCallsByConversationId = initialJoinableCallsByConversationId, + joinableCallsFlow = joinableCallsFlow + ) + + ConversationFilter.OneOnOne -> + useCase( + queryConfig = ConversationQueryConfig( + searchQuery = searchQuery, + fromArchive = fromArchive, + newActivitiesOnTop = newActivitiesOnTop, + onlyInteractionEnabled = onlyInteractionEnabled, + conversationFilter = conversationFilter, + ), + pagingConfig = pagingConfig, + startingOffset = 0L, + strictMlsFilter = useStrictMlsFilter + ).mapToConversationItems( + selfUserTeamId = selfUserTeamId, + initialJoinableCallsByConversationId = emptyMap(), + joinableCallsFlow = flowOf(emptyMap()) + ) ConversationFilter.Favorites -> { when (val result = getFavoriteFolderUseCase.invoke()) { @@ -92,27 +124,70 @@ class GetConversationsFromSearchUseCase @Inject constructor( is GetFavoriteFolderUseCase.Result.Success -> observeConversationsFromFromFolder(result.folder.id) } - .map { staticPagingItems(it) } + .combine(joinableCallsFlow) { conversations, joinableCallsByConversationId -> + staticPagingItems( + conversations.toConversationItems( + selfUserTeamId = selfUserTeamId, + joinableCallsByConversationId = joinableCallsByConversationId + ) + ) + } } is ConversationFilter.Folder -> { observeConversationsFromFromFolder(conversationFilter.folderId) - .map { staticPagingItems(it) } + .combine(joinableCallsFlow) { conversations, joinableCallsByConversationId -> + staticPagingItems( + conversations.toConversationItems( + selfUserTeamId = selfUserTeamId, + joinableCallsByConversationId = joinableCallsByConversationId + ) + ) + } } + }.flowOn(dispatchers.io()) + } + + private fun Flow>.mapToConversationItems( + selfUserTeamId: TeamId?, + initialJoinableCallsByConversationId: Map, + joinableCallsFlow: Flow> + ): Flow> = channelFlow { + val latestJoinableCallsByConversationId = MutableStateFlow(initialJoinableCallsByConversationId) + val callsJob = launch { + joinableCallsFlow.collect { latestJoinableCallsByConversationId.value = it } } - return combine(conversationDetailsFlow, observeJoinableCalls()) { pagingData, joinableCallsByConversationId -> - pagingData.map { - it.toConversationItem( - userTypeMapper = userTypeMapper, - uiTextResolver = uiTextResolver, - selfUserTeamId = selfUserTeamId, - joinableCallsByConversationId = joinableCallsByConversationId + try { + collect { pagingData -> + send( + pagingData.map { + it.toConversationItem( + userTypeMapper = userTypeMapper, + uiTextResolver = uiTextResolver, + selfUserTeamId = selfUserTeamId, + joinableCallsByConversationId = latestJoinableCallsByConversationId.value + ) + } ) } - }.flowOn(dispatchers.io()) + } finally { + callsJob.cancel() + } + } + + private fun List.toConversationItems( + selfUserTeamId: TeamId?, + joinableCallsByConversationId: Map + ): List = map { + it.toConversationItem( + userTypeMapper = userTypeMapper, + uiTextResolver = uiTextResolver, + selfUserTeamId = selfUserTeamId, + joinableCallsByConversationId = joinableCallsByConversationId + ) } - private fun staticPagingItems(conversations: List): PagingData { + private fun staticPagingItems(conversations: List): PagingData { return PagingData.from( conversations, sourceLoadStates = LoadStates( diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt index dc79b3385a2..b7c505a6938 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationsFromSearchUseCaseTest.kt @@ -47,7 +47,9 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -218,6 +220,37 @@ class GetConversationsFromSearchUseCaseTest { assertEquals(true, group.hasNewActivitiesToShow) } + @Test + fun givenJoinableCallsChange_whenGettingPaginatedList_thenDoNotReEmitSamePagingData() = + runTest(dispatcherProvider.main()) { + // Given + val conversation = ConversationDetailsWithEvents(TestConversationDetails.GROUP) + val call = joinableCall(conversation.conversationDetails.conversation.id) + val (_, useCase) = Arrangement() + .withPaginatedResult(listOf(conversation)) + .withJoinableCallsFlow( + flowOf( + mapOf(call.conversationId to call), + emptyMap() + ) + ) + .arrange() + + // When + val result = with(ConversationQueryConfig("search")) { + useCase( + searchQuery = searchQuery, + fromArchive = fromArchive, + newActivitiesOnTop = newActivitiesOnTop, + onlyInteractionEnabled = onlyInteractionEnabled, + useStrictMlsFilter = true + ).toList() + } + + // Then + assertEquals(1, result.size) + } + inner class Arrangement { @MockK @@ -285,6 +318,10 @@ class GetConversationsFromSearchUseCaseTest { coEvery { observeJoinableCalls() } returns flowOf(calls.associateBy { it.conversationId }) } + fun withJoinableCallsFlow(result: Flow>) = apply { + coEvery { observeJoinableCalls() } returns result + } + fun withSelfTeamId() = apply { coEvery { getSelfTeamId() } returns TestUser.SELF_USER.teamId }