From 76b8918162d317d853596898a1bb3a9a97818847 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Mon, 18 May 2026 19:06:05 +0200 Subject: [PATCH 1/4] feat: enhance call management with active call tracking and improved flow handling --- .../usecase/GetConversationsFromSearchUseCase.kt | 3 ++- .../home/conversationslist/ConversationListViewModel.kt | 8 ++++++-- .../usecase/GetConversationsFromSearchUseCaseTest.kt | 2 +- 3 files changed, 9 insertions(+), 4 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 1e0e44dae1c..72cd40b5d8a 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 @@ -108,7 +108,8 @@ class GetConversationsFromSearchUseCase @Inject constructor( playingAudioMessage = playingAudioMessage ) } - }.flowOn(dispatchers.io()) + } + .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 f880049a59f..0710a05e7dc 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 @@ -231,7 +231,8 @@ class ConversationListViewModelImpl @AssistedInject constructor( searchQuery = searchQuery, selfUserTeamId = getSelfUser()?.teamId, playingAudioMessage = playingAudioMessage - ).hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold) + ) + .hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold) } to searchQuery } } @@ -398,12 +399,15 @@ private fun List.unreadToReadConversationsItems(): Pair false } || (it is ConversationItem.Group && it.hasOnGoingCall) - } + }.sortedByDescending { it.isActiveGroupCall } val remainingConversations = this - unreadConversations.toSet() return unreadConversations to remainingConversations } +private val ConversationItem.isActiveGroupCall: Boolean + get() = this is ConversationItem.Group && hasOnGoingCall + private fun searchConversation(conversationDetails: List, searchQuery: String): List = conversationDetails.filter { details -> when (details) { 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 fc711fe2fb9..797b31303e1 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 @@ -66,7 +66,7 @@ class GetConversationsFromSearchUseCaseTest { newActivitiesOnTop = newActivitiesOnTop, onlyInteractionEnabled = onlyInteractionEnabled, useStrictMlsFilter = true - ) + ).asSnapshot() } // Then coVerify { From 10c824fcff1f25b37cb87a549bac67e412483f11 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 17:01:59 +0200 Subject: [PATCH 2/4] feat: add functionality to observe active call conversation IDs and update related data structures --- .../android/di/accountScoped/CallsModule.kt | 5 +++ .../wire/android/mapper/ConversationMapper.kt | 4 +- .../ConversationListState.kt | 6 ++- .../ConversationListViewModel.kt | 20 ++++++++- .../ConversationsScreenContent.kt | 5 +++ .../ConversationItemActiveCallStatus.kt | 44 +++++++++++++++++++ .../common/ConversationList.kt | 19 +++++--- .../MessageComposerViewModelArrangement.kt | 1 - .../details/GroupDetailsViewModelTest.kt | 2 - .../UpdateAppsAccessViewModelTest.kt | 1 - .../ConversationListViewModelTest.kt | 6 +++ kalium | 2 +- 12 files changed, 98 insertions(+), 17 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemActiveCallStatus.kt 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 e18515964e8..5c361e0f054 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 @@ -77,6 +77,11 @@ class CallsModule { fun provideObserveOngoingCallsUseCase(callsScope: CallsScope) = callsScope.observeOngoingCalls + @ViewModelScoped + @Provides + fun provideObserveActiveCallConversationIdsUseCase(callsScope: CallsScope) = + callsScope.observeActiveCallConversationIds + @ViewModelScoped @Provides fun provideObserveEstablishedCallWithSortedParticipantsUseCase(callsScope: CallsScope) = 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 b46424ed272..5305a5712f7 100644 --- a/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt +++ b/app/src/main/kotlin/com/wire/android/mapper/ConversationMapper.kt @@ -62,7 +62,7 @@ fun ConversationDetailsWithEvents.toConversationItem( mutedStatus = conversationDetails.conversation.mutedStatus, unreadEventCount = unreadEventCount ), - hasOnGoingCall = conversationDetails.hasOngoingCall && conversationDetails.isSelfUserMember, + hasOnGoingCall = false, isFromTheSameTeam = conversationDetails.conversation.teamId == selfUserTeamId, isSelfUserMember = conversationDetails.isSelfUserMember, teamId = conversationDetails.conversation.teamId, @@ -89,7 +89,7 @@ fun ConversationDetailsWithEvents.toConversationItem( mutedStatus = conversationDetails.conversation.mutedStatus, unreadEventCount = unreadEventCount ), - hasOnGoingCall = conversationDetails.hasOngoingCall && conversationDetails.isSelfUserMember, + hasOnGoingCall = false, isFromTheSameTeam = conversationDetails.conversation.teamId == selfUserTeamId, isSelfUserMember = conversationDetails.isSelfUserMember, teamId = conversationDetails.conversation.teamId, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListState.kt index 99c6714f778..b06c15de274 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListState.kt @@ -20,22 +20,26 @@ package com.wire.android.ui.home.conversationslist import androidx.compose.runtime.Stable import androidx.paging.PagingData +import com.wire.android.ui.home.conversationslist.model.ConversationItem import com.wire.android.ui.home.conversationslist.model.ConversationSection import com.wire.android.ui.home.conversationslist.model.ConversationItemType -import com.wire.android.ui.home.conversationslist.model.ConversationItem +import com.wire.kalium.logic.data.id.ConversationId import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf @Stable sealed interface ConversationListState { data class Paginated( val conversations: Flow>, + val activeCallConversationIds: Flow> = flowOf(emptySet()), val domain: String = "", ) : ConversationListState data class NotPaginated( val isLoading: Boolean = true, val conversations: ImmutableMap> = persistentMapOf(), + val activeCallConversationIds: Flow> = flowOf(emptySet()), val domain: String = "", ) : ConversationListState } 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 0710a05e7dc..663d9f46fb5 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 @@ -48,7 +48,9 @@ import com.wire.android.util.ui.UiTextResolver 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.id.ConversationId import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.call.usecase.ObserveActiveCallConversationIdsUseCase import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsWithEventsUseCase import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase @@ -70,7 +72,9 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -104,6 +108,7 @@ class ConversationListViewModelImpl @AssistedInject constructor( private val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase, private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, private val refreshConversationsWithoutMetadata: RefreshConversationsWithoutMetadataUseCase, + private val observeActiveCallConversationIds: ObserveActiveCallConversationIdsUseCase, private val observeLegalHoldStateForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, private val audioMessagePlayer: ConversationAudioMessagePlayer, @CurrentAccount val currentAccount: UserId, @@ -128,6 +133,11 @@ class ConversationListViewModelImpl @AssistedInject constructor( private val searchQueryFlow: MutableStateFlow = MutableStateFlow("") private val isSelfUserUnderLegalHoldFlow = MutableSharedFlow(replay = 1) + private val activeCallConversationIdsFlow: Flow> = flow { + emitAll(observeActiveCallConversationIds()) + } + .onStart { emit(emptySet()) } + .flowOn(dispatcher.io()) private val containsNewActivitiesSection = when (conversationsSource) { ConversationsSource.MAIN, @@ -187,8 +197,13 @@ class ConversationListViewModelImpl @AssistedInject constructor( override var conversationListState by mutableStateOf( when (usePagination) { - true -> ConversationListState.Paginated(conversations = conversationsPaginatedFlow, domain = currentAccount.domain) - false -> ConversationListState.NotPaginated() + true -> ConversationListState.Paginated( + conversations = conversationsPaginatedFlow, + activeCallConversationIds = activeCallConversationIdsFlow, + domain = currentAccount.domain + ) + + false -> ConversationListState.NotPaginated(activeCallConversationIds = activeCallConversationIdsFlow) } ) private set @@ -251,6 +266,7 @@ class ConversationListViewModelImpl @AssistedInject constructor( conversationListState = ConversationListState.NotPaginated( isLoading = false, conversations = it, + activeCallConversationIds = activeCallConversationIdsFlow, domain = currentAccount.domain ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt index 1f75bca5bf1..015a4838891 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationsScreenContent.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import com.ramcosta.composedestinations.generated.app.destinations.BrowseChannelsScreenDestination @@ -167,6 +168,7 @@ fun ConversationsScreenContent( when (val state = conversationListViewModel.conversationListState) { is ConversationListState.Paginated -> { val lazyPagingItems = state.conversations.collectAsLazyPagingItemsWithLifecycle() + val activeCallConversationIds by state.activeCallConversationIds.collectAsStateWithLifecycle(emptySet()) searchBarState.searchVisibleChanged(lazyPagingItems.itemCount > 0 || searchBarState.isSearchActive) when { // when conversation list is not yet fetched, show loading indicator @@ -179,6 +181,7 @@ fun ConversationsScreenContent( onEditConversation = onEditConversationItem, onOpenUserProfile = onOpenUserProfile, onJoinCall = onJoinCall, + activeCallConversationIds = activeCallConversationIds, onAudioPermissionPermanentlyDenied = { permissionPermanentlyDeniedDialogState.show( PermissionPermanentlyDeniedDialogState.Visible( @@ -200,6 +203,7 @@ fun ConversationsScreenContent( } is ConversationListState.NotPaginated -> { + val activeCallConversationIds by state.activeCallConversationIds.collectAsStateWithLifecycle(emptySet()) val hasConversations = state.conversations.isNotEmpty() && state.conversations.any { it.value.isNotEmpty() } searchBarState.searchVisibleChanged(isSearchVisible = hasConversations || searchBarState.isSearchActive) when { @@ -213,6 +217,7 @@ fun ConversationsScreenContent( onEditConversation = onEditConversationItem, onOpenUserProfile = onOpenUserProfile, onJoinCall = onJoinCall, + activeCallConversationIds = activeCallConversationIds, onAudioPermissionPermanentlyDenied = { permissionPermanentlyDeniedDialogState.show( PermissionPermanentlyDeniedDialogState.Visible( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemActiveCallStatus.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemActiveCallStatus.kt new file mode 100644 index 00000000000..4583b0897ab --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemActiveCallStatus.kt @@ -0,0 +1,44 @@ +/* + * 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.android.ui.home.conversationslist.common + +import com.wire.android.ui.home.conversationslist.model.ConversationItem +import com.wire.kalium.logic.data.id.ConversationId + +internal fun ConversationItem.withActiveCallStatus(activeCallConversationIds: Set): ConversationItem = + when (this) { + is ConversationItem.Group.Regular -> { + val hasActiveCall = conversationId in activeCallConversationIds && isSelfUserMember + copy( + hasOnGoingCall = hasActiveCall, + hasNewActivitiesToShow = hasNewActivitiesToShow || hasActiveCall + ) + } + + is ConversationItem.Group.Channel -> { + val hasActiveCall = conversationId in activeCallConversationIds && isSelfUserMember + copy( + hasOnGoingCall = hasActiveCall, + hasNewActivitiesToShow = hasNewActivitiesToShow || hasActiveCall + ) + } + + is ConversationItem.ConnectionConversation, + is ConversationItem.PrivateConversation -> this + } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt index d38a9eec1a4..fdda12c2296 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationList.kt @@ -88,6 +88,7 @@ fun ConversationList( onEditConversation: (ConversationItem) -> Unit = {}, onOpenUserProfile: (UserId) -> Unit = {}, onJoinCall: (ConversationId) -> Unit = {}, + activeCallConversationIds: Set = emptySet(), onConversationSelectedOnRadioGroup: (ConversationItem) -> Unit = {}, onAudioPermissionPermanentlyDenied: () -> Unit = {}, onPlayPauseCurrentAudio: () -> Unit = { }, @@ -138,12 +139,13 @@ fun ConversationList( is ConversationSection.WithoutHeader -> {} } - is ConversationItem -> + is ConversationItem -> { + val conversation = item.withActiveCallStatus(activeCallConversationIds) ConversationItemFactory( - conversation = item, + conversation = conversation, isSelectableItem = isSelectableList, - isChecked = selectedConversations.contains(item.conversationId), - onConversationSelectedOnRadioGroup = { onConversationSelectedOnRadioGroup(item) }, + isChecked = selectedConversations.contains(conversation.conversationId), + onConversationSelectedOnRadioGroup = { onConversationSelectedOnRadioGroup(conversation) }, openConversation = onOpenConversation, openMenu = onEditConversation, openUserProfile = onOpenUserProfile, @@ -152,6 +154,7 @@ fun ConversationList( onPlayPauseCurrentAudio = onPlayPauseCurrentAudio, onStopCurrentAudio = onStopCurrentAudio ) + } else -> {} } @@ -213,6 +216,7 @@ fun ConversationList( onEditConversation: (ConversationItem) -> Unit = {}, onOpenUserProfile: (UserId) -> Unit = {}, onJoinCall: (ConversationId) -> Unit = {}, + activeCallConversationIds: Set = emptySet(), onConversationSelectedOnRadioGroup: (ConversationId) -> Unit = {}, onAudioPermissionPermanentlyDenied: () -> Unit = {}, onPlayPauseCurrentAudio: () -> Unit = { }, @@ -233,11 +237,12 @@ fun ConversationList( it.conversationId.toString() } ) { generalConversation -> + val conversation = generalConversation.withActiveCallStatus(activeCallConversationIds) ConversationItemFactory( - conversation = generalConversation, + conversation = conversation, isSelectableItem = isSelectableList, - isChecked = selectedConversations.contains(generalConversation), - onConversationSelectedOnRadioGroup = { onConversationSelectedOnRadioGroup(generalConversation.conversationId) }, + isChecked = selectedConversations.any { it.conversationId == conversation.conversationId }, + onConversationSelectedOnRadioGroup = { onConversationSelectedOnRadioGroup(conversation.conversationId) }, openConversation = onOpenConversation, openMenu = onEditConversation, openUserProfile = onOpenUserProfile, 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 d598fedad15..974499f4730 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 @@ -227,7 +227,6 @@ internal fun mockConversationDetailsGroup( ) = ConversationDetails.Group.Regular( conversation = TestConversation.GROUP() .copy(name = conversationName, id = mockedConversationId), - 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/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, 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 b87360ff455..b3517145665 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 @@ -41,6 +41,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.ObserveActiveCallConversationIdsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsWithEventsUseCase import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser @@ -287,6 +288,9 @@ class ConversationListViewModelTest { @MockK private lateinit var observeLegalHoldStateForSelfUserUseCase: ObserveLegalHoldStateForSelfUserUseCase + @MockK + private lateinit var observeActiveCallConversationIdsUseCase: ObserveActiveCallConversationIdsUseCase + @MockK private lateinit var getSelfUser: GetSelfUserUseCase @@ -312,6 +316,7 @@ class ConversationListViewModelTest { } ) every { audioMessagePlayer.playingAudioMessageFlow } returns flowOf(PlayingAudioMessage.None) + coEvery { observeActiveCallConversationIdsUseCase() } returns flowOf(emptySet()) coEvery { uiTextResolver.resolve(any()) } answers { val text = firstArg() when (text) { @@ -356,6 +361,7 @@ class ConversationListViewModelTest { currentAccount = TestUser.SELF_USER_ID, observeConversationListDetailsWithEvents = observeConversationListDetailsWithEventsUseCase, observeLegalHoldStateForSelfUser = observeLegalHoldStateForSelfUserUseCase, + observeActiveCallConversationIds = observeActiveCallConversationIdsUseCase, userTypeMapper = UserTypeMapper(), getSelfUser = getSelfUser, uiTextResolver = uiTextResolver, diff --git a/kalium b/kalium index b3d20a1dcbc..c36d3bc7405 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit b3d20a1dcbc25520829fdd12fcc52107b3f5df95 +Subproject commit c36d3bc7405e4f14ccb07e0ed7ecfe216a4da91d From b9e3c42d55e28473f219ff4bc23b4728253256b7 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 17:06:39 +0200 Subject: [PATCH 3/4] detekt --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index c36d3bc7405..e9d6c97f96e 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit c36d3bc7405e4f14ccb07e0ed7ecfe216a4da91d +Subproject commit e9d6c97f96e5855edc5b4bbfd5a7292f34b30188 From 2a42015a991bb09031028097b8a031d522bffc0d Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 21:35:13 +0200 Subject: [PATCH 4/4] feat: enhance conversation list management with active call tracking and new activities handling --- .../ConversationListViewModel.kt | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) 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 663d9f46fb5..ed619fac75c 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 @@ -158,7 +158,11 @@ class ConversationListViewModelImpl @AssistedInject constructor( .combine(audioMessagePlayer.playingAudioMessageFlow) { (searchQuery, isSelfUserUnderLegalHold), playingAudioMessage -> Triple(searchQuery, isSelfUserUnderLegalHold, playingAudioMessage) } - .flatMapLatest { (searchQuery, isSelfUserUnderLegalHold, playingAudioMessage) -> + .combine(activeCallConversationIdsFlow) { conversationListConfig, activeCallConversationIds -> + conversationListConfig to activeCallConversationIds + } + .flatMapLatest { (conversationListConfig, activeCallConversationIds) -> + val (searchQuery, isSelfUserUnderLegalHold, playingAudioMessage) = conversationListConfig getConversationsPaginated( searchQuery = searchQuery, fromArchive = conversationsSource == ConversationsSource.ARCHIVE, @@ -175,15 +179,18 @@ class ConversationListViewModelImpl @AssistedInject constructor( // do not add separators if the list shouldn't show conversations grouped into different folders !containsNewActivitiesSection -> null - before == null && after != null && after.hasNewActivitiesToShow -> + before == null && after != null && after.hasNewActivitiesToShow(activeCallConversationIds) -> // list starts with items with "new activities" ConversationSection.Predefined.NewActivities - before == null && after != null && !after.hasNewActivitiesToShow -> + before == null && after != null && !after.hasNewActivitiesToShow(activeCallConversationIds) -> // list doesn't contain any items with "new activities" ConversationSection.Predefined.Conversations - before != null && before.hasNewActivitiesToShow && after != null && !after.hasNewActivitiesToShow -> + before != null && + before.hasNewActivitiesToShow(activeCallConversationIds) && + after != null && + !after.hasNewActivitiesToShow(activeCallConversationIds) -> // end of "new activities" section and beginning of "conversations" section ConversationSection.Predefined.Conversations @@ -237,8 +244,9 @@ class ConversationListViewModelImpl @AssistedInject constructor( conversationFilter = conversationsSource.toFilter() ), isSelfUserUnderLegalHoldFlow, - audioMessagePlayer.playingAudioMessageFlow - ) { conversations, isSelfUserUnderLegalHold, playingAudioMessage -> + audioMessagePlayer.playingAudioMessageFlow, + activeCallConversationIdsFlow + ) { conversations, isSelfUserUnderLegalHold, playingAudioMessage, activeCallConversationIds -> conversations.map { conversationDetails -> conversationDetails.toConversationItem( userTypeMapper = userTypeMapper, @@ -248,17 +256,24 @@ class ConversationListViewModelImpl @AssistedInject constructor( playingAudioMessage = playingAudioMessage ) .hideIndicatorForSelfUserUnderLegalHold(isSelfUserUnderLegalHold) - } to searchQuery + } to (searchQuery to activeCallConversationIds) } } - .map { (conversationItems, searchQuery) -> + .map { (conversationItems, searchQueryAndActiveCallConversationIds) -> + val (searchQuery, activeCallConversationIds) = searchQueryAndActiveCallConversationIds if (searchQuery.isEmpty()) { - conversationItems.withSections(source = conversationsSource).toImmutableMap() + conversationItems.withSections( + source = conversationsSource, + activeCallConversationIds = activeCallConversationIds + ).toImmutableMap() } else { searchConversation( conversationDetails = conversationItems, searchQuery = searchQuery - ).withSections(source = conversationsSource).toImmutableMap() + ).withSections( + source = conversationsSource, + activeCallConversationIds = activeCallConversationIds + ).toImmutableMap() } } .flowOn(dispatcher.io()) @@ -346,7 +361,10 @@ private fun ConversationItem.hideIndicatorForSelfUserUnderLegalHold(isSelfUserUn } @Suppress("ComplexMethod") -private fun List.withSections(source: ConversationsSource): Map> { +private fun List.withSections( + source: ConversationsSource, + activeCallConversationIds: Set = emptySet() +): Map> { return when (source) { ConversationsSource.ARCHIVE -> { buildMap { @@ -362,7 +380,7 @@ private fun List.withSections(source: ConversationsSource): Ma ConversationsSource.ONE_ON_ONE, is ConversationsSource.FOLDER, ConversationsSource.MAIN -> { - val (unreadConversations, remainingConversations) = unreadToReadConversationsItems() + val (unreadConversations, remainingConversations) = unreadToReadConversationsItems(activeCallConversationIds) buildMap { if (unreadConversations.isNotEmpty()) { put(ConversationSection.Predefined.NewActivities, unreadConversations) @@ -374,7 +392,7 @@ private fun List.withSections(source: ConversationsSource): Ma } is ConversationsSource.CHANNELS -> { - val (unreadConversations, remainingConversations) = unreadToReadConversationsItems() + val (unreadConversations, remainingConversations) = unreadToReadConversationsItems(activeCallConversationIds) buildMap { put(ConversationSection.Predefined.BrowseChannels, emptyList()) if (unreadConversations.isNotEmpty()) { @@ -389,7 +407,9 @@ private fun List.withSections(source: ConversationsSource): Ma } @Suppress("CyclomaticComplexMethod") -private fun List.unreadToReadConversationsItems(): Pair, List> { +private fun List.unreadToReadConversationsItems( + activeCallConversationIds: Set +): Pair, List> { val unreadConversations = filter { when (it.mutedStatus) { MutedConversationStatus.AllAllowed -> when (it.badgeEventType) { @@ -414,15 +434,21 @@ private fun List.unreadToReadConversationsItems(): Pair false - } || (it is ConversationItem.Group && it.hasOnGoingCall) - }.sortedByDescending { it.isActiveGroupCall } + } || it.isActiveGroupCall(activeCallConversationIds) + }.sortedByDescending { it.isActiveGroupCall(activeCallConversationIds) } val remainingConversations = this - unreadConversations.toSet() return unreadConversations to remainingConversations } -private val ConversationItem.isActiveGroupCall: Boolean - get() = this is ConversationItem.Group && hasOnGoingCall +private fun ConversationItemType.hasNewActivitiesToShow(activeCallConversationIds: Set): Boolean = + this is ConversationItem && hasNewActivitiesToShow(activeCallConversationIds) + +private fun ConversationItem.hasNewActivitiesToShow(activeCallConversationIds: Set): Boolean = + hasNewActivitiesToShow || isActiveGroupCall(activeCallConversationIds) + +private fun ConversationItem.isActiveGroupCall(activeCallConversationIds: Set): Boolean = + this is ConversationItem.Group && isSelfUserMember && conversationId in activeCallConversationIds private fun searchConversation(conversationDetails: List, searchQuery: String): List = conversationDetails.filter { details ->