From 2dd76820540fd825bf85d33dcc796d645bfc1f93 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:17:19 +0200 Subject: [PATCH 1/7] feat: implement SyncServicesUseCase to sync services from backend and update ObserveAllServicesUseCase to avoid network calls --- .../di/accountScoped/ServicesModule.kt | 6 ++ .../search/apps/SearchAppsViewModel.kt | 7 ++ .../search/apps/SearchAppsViewModelTest.kt | 72 +++++++++++++++++++ kalium | 2 +- 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/ServicesModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/ServicesModule.kt index b95b9de878e..f3b7c07adae 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ServicesModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/ServicesModule.kt @@ -26,6 +26,7 @@ import com.wire.kalium.logic.feature.service.ObserveAllServicesUseCase import com.wire.kalium.logic.feature.service.ObserveIsServiceMemberUseCase import com.wire.kalium.logic.feature.service.SearchServicesByNameUseCase import com.wire.kalium.logic.feature.service.ServiceScope +import com.wire.kalium.logic.feature.service.SyncServicesUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -58,6 +59,11 @@ class ServicesModule { fun provideObserveAllServicesUseCase(serviceScope: ServiceScope): ObserveAllServicesUseCase = serviceScope.observeAllServices + @ViewModelScoped + @Provides + fun provideSyncServicesUseCase(serviceScope: ServiceScope): SyncServicesUseCase = + serviceScope.syncServices + @ViewModelScoped @Provides fun provideSearchServicesByNameUseCase(serviceScope: ServiceScope): SearchServicesByNameUseCase = diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModel.kt index 702e332b5f6..793e4d438aa 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModel.kt @@ -35,6 +35,7 @@ import com.wire.kalium.logic.feature.featureConfig.AppsAllowedResult import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase import com.wire.kalium.logic.feature.service.ObserveAllServicesUseCase import com.wire.kalium.logic.feature.service.SearchServicesByNameUseCase +import com.wire.kalium.logic.feature.service.SyncServicesUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -55,6 +56,7 @@ import kotlinx.coroutines.launch class SearchAppsViewModel @AssistedInject constructor( @Assisted val protocolInfo: Conversation.ProtocolInfo?, private val getAllServices: ObserveAllServicesUseCase, + private val syncServices: SyncServicesUseCase, private val getAllApps: ObserveAllAppsUseCase, private val contactMapper: ContactMapper, private val searchServicesByName: SearchServicesByNameUseCase, @@ -63,6 +65,7 @@ class SearchAppsViewModel @AssistedInject constructor( private val observeSelfUser: ObserveSelfUserUseCase ) : ViewModel() { private val searchQueryTextFlow = MutableStateFlow(String.EMPTY) + private var servicesSynced = false var state: SearchServicesState by mutableStateOf(SearchServicesState(isLoading = true)) private set @@ -113,6 +116,10 @@ class SearchAppsViewModel @AssistedInject constructor( val result = if (showNewApps) { if (query.isEmpty()) getAllApps() else searchAppsByName(query) } else { + if (!servicesSynced) { + servicesSynced = true + launch { syncServices() } + } if (query.isEmpty()) getAllServices() else searchServicesByName(query) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt index 22a3730ae7e..33493a444eb 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt @@ -38,8 +38,11 @@ import com.wire.kalium.logic.feature.app.SearchAppsByNameUseCase import com.wire.kalium.logic.feature.featureConfig.AppsAllowedProtocol import com.wire.kalium.logic.feature.featureConfig.AppsAllowedResult import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase +import com.wire.kalium.common.error.NetworkFailure +import com.wire.kalium.common.functional.Either import com.wire.kalium.logic.feature.service.ObserveAllServicesUseCase import com.wire.kalium.logic.feature.service.SearchServicesByNameUseCase +import com.wire.kalium.logic.feature.service.SyncServicesUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -266,6 +269,66 @@ class SearchAppsViewModelTest { assertEquals(1, viewModel.state.result.size) } + @Test + fun `given services branch is used across multiple searches, when init view model, then syncServices is called exactly once`() = + runTest { + // given + val query = "Service Name" + val (arrangement, viewModel) = Arrangement() + .withAppsAllowedForUsage(AppsAllowedResult.Enabled(AppsAllowedProtocol.PROTEUS)) + .withGetAllServices(listOf(SERVICE_DETAILS)) + .withSearchServicesByName(query, listOf(SERVICE_DETAILS)) + .arrange(protocolInfo = null) + + // when + advanceUntilIdle() + viewModel.searchQueryChanged(query) + advanceUntilIdle() + viewModel.searchQueryChanged(String.EMPTY) + advanceUntilIdle() + + // then + coVerify(exactly = 1) { + arrangement.syncServices() + } + } + + @Test + fun `given apps branch is used, when init view model, then syncServices is never called`() = + runTest { + // given + val (arrangement, _) = Arrangement() + .withAppsAllowedForUsage(AppsAllowedResult.Enabled(AppsAllowedProtocol.MLS)) + .withGetAllApps(listOf(SERVICE_DETAILS)) + .arrange(protocolInfo = null) + + // when + advanceUntilIdle() + + // then + coVerify(exactly = 0) { + arrangement.syncServices() + } + } + + @Test + fun `given syncServices fails, when services branch is used, then observed services are still emitted`() = + runTest { + // given + val (_, viewModel) = Arrangement() + .withAppsAllowedForUsage(AppsAllowedResult.Enabled(AppsAllowedProtocol.PROTEUS)) + .withGetAllServices(listOf(SERVICE_DETAILS, SERVICE_DETAILS2)) + .withSyncServicesFailing() + .arrange(protocolInfo = null) + + // when + advanceUntilIdle() + + // then + assertEquals(2, viewModel.state.result.size) + assertFalse(viewModel.state.isLoading) + } + @Test fun `given Apps feature flag is PROTEUS enabled, when searching for Apps by name, then load searched Services`() = runTest { @@ -340,6 +403,9 @@ class SearchAppsViewModelTest { @MockK lateinit var getAllServices: ObserveAllServicesUseCase + @MockK + lateinit var syncServices: SyncServicesUseCase + @MockK lateinit var getAllApps: ObserveAllAppsUseCase @@ -362,6 +428,7 @@ class SearchAppsViewModelTest { MockKAnnotations.init(this, relaxUnitFun = true) coEvery { getAllServices() } returns flowOf(emptyList()) + coEvery { syncServices() } returns Either.Right(Unit) coEvery { getAllApps() } returns flowOf(emptyList()) coEvery { searchServicesByName(any()) } returns flowOf(emptyList()) coEvery { searchAppsByName(any()) } returns flowOf(emptyList()) @@ -375,6 +442,7 @@ class SearchAppsViewModelTest { fun arrange(protocolInfo: Conversation.ProtocolInfo?) = this to SearchAppsViewModel( protocolInfo = protocolInfo, getAllServices = getAllServices, + syncServices = syncServices, getAllApps = getAllApps, contactMapper = contactMapper, searchServicesByName = searchServicesByName, @@ -402,5 +470,9 @@ class SearchAppsViewModelTest { fun withSearchServicesByName(query: String, result: List) = apply { coEvery { searchServicesByName(query) } returns flowOf(result) } + + fun withSyncServicesFailing() = apply { + coEvery { syncServices() } returns Either.Left(NetworkFailure.NoNetworkConnection(cause = null)) + } } } diff --git a/kalium b/kalium index 9a0b40b5dc7..ff092ed3949 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 9a0b40b5dc7cbf73dc156fc690222a18d4673759 +Subproject commit ff092ed39499d6a5063132488d8a62593ae7b543 From d6e6d82b7840bf1833b97e8c3bb24a80b2db6ff5 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 14:22:38 +0200 Subject: [PATCH 2/7] update kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index ff092ed3949..8dbbadec7fe 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit ff092ed39499d6a5063132488d8a62593ae7b543 +Subproject commit 8dbbadec7fe7b18f90ea801872f966a981e44861 From 9d30ae539a120a49b3c142c1af1891a2fe3442e7 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 14:37:43 +0200 Subject: [PATCH 3/7] tests --- .../home/conversations/search/apps/SearchAppsViewModelTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt index 33493a444eb..68189822f06 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt @@ -428,7 +428,7 @@ class SearchAppsViewModelTest { MockKAnnotations.init(this, relaxUnitFun = true) coEvery { getAllServices() } returns flowOf(emptyList()) - coEvery { syncServices() } returns Either.Right(Unit) + coEvery { syncServices() } returns SyncServicesUseCase.Result.Success coEvery { getAllApps() } returns flowOf(emptyList()) coEvery { searchServicesByName(any()) } returns flowOf(emptyList()) coEvery { searchAppsByName(any()) } returns flowOf(emptyList()) @@ -472,7 +472,7 @@ class SearchAppsViewModelTest { } fun withSyncServicesFailing() = apply { - coEvery { syncServices() } returns Either.Left(NetworkFailure.NoNetworkConnection(cause = null)) + coEvery { syncServices() } returns SyncServicesUseCase.Result.Failure(NetworkFailure.NoNetworkConnection(cause = null)) } } } From 30878e4f8f274359c5fe4c122b38c8e69283c47c Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 14:43:21 +0200 Subject: [PATCH 4/7] CI --- .github/workflows/jira-lint-and-link.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/jira-lint-and-link.yml b/.github/workflows/jira-lint-and-link.yml index e9d8dad1cef..67c28a17b23 100644 --- a/.github/workflows/jira-lint-and-link.yml +++ b/.github/workflows/jira-lint-and-link.yml @@ -17,9 +17,12 @@ jobs: steps: - name: Get version name from source id: version + env: + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref || github.ref_name }} run: | - VERSION=$(curl -s https://raw.githubusercontent.com/${{ github.repository }}/${{ github.head_ref || github.ref_name }}/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt | grep 'const val versionName' | sed -E 's/.*"([^"]+)".*/\1/') - echo "version_name=$VERSION" >> $GITHUB_OUTPUT + VERSION=$(curl -s "https://raw.githubusercontent.com/${REPO}/${BRANCH}/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt" | grep 'const val versionName' | sed -E 's/.*"([^"]+)".*/\1/') + echo "version_name=$VERSION" >> "$GITHUB_OUTPUT" add-jira-description: if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} From c4c55a790bde4bb12ab6f4b6196f5c216185942a Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 14:55:07 +0200 Subject: [PATCH 5/7] detekt --- .../ui/home/conversations/search/apps/SearchAppsViewModelTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt index 68189822f06..dd5a3ab925d 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelTest.kt @@ -39,7 +39,6 @@ import com.wire.kalium.logic.feature.featureConfig.AppsAllowedProtocol import com.wire.kalium.logic.feature.featureConfig.AppsAllowedResult import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase import com.wire.kalium.common.error.NetworkFailure -import com.wire.kalium.common.functional.Either import com.wire.kalium.logic.feature.service.ObserveAllServicesUseCase import com.wire.kalium.logic.feature.service.SearchServicesByNameUseCase import com.wire.kalium.logic.feature.service.SyncServicesUseCase From 1b0465688ef9be8957be3d678845ea07cef7c4b6 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 15:28:42 +0200 Subject: [PATCH 6/7] kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 8dbbadec7fe..b3d20a1dcbc 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 8dbbadec7fe7b18f90ea801872f966a981e44861 +Subproject commit b3d20a1dcbc25520829fdd12fcc52107b3f5df95 From 20a5c488d9be363c3beba0646fe2bf85bf8c3996 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara <9083456+MohamadJaara@users.noreply.github.com> Date: Tue, 19 May 2026 15:29:54 +0200 Subject: [PATCH 7/7] triogger CI