diff --git a/.java-version b/.java-version index 03b6389f32a..5f39e914469 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -17.0 +21.0 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3fe03d4ad2c..155dbb2d483 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,7 +26,6 @@ plugins { // id(BuildPlugins.kotlinAndroidExtensions) id(BuildPlugins.kotlinParcelize) id(BuildPlugins.junit5) - id(libs.plugins.wire.hilt.get().pluginId) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) alias(libs.plugins.compose.compiler) @@ -202,6 +201,7 @@ dependencies { implementationWithCoverage(projects.features.sketch) implementationWithCoverage(projects.features.meetings) implementationWithCoverage(projects.features.sync) + implementation(projects.shared.auth) // Anonymous Analytics val flavors = getFlavorsSettings() @@ -274,14 +274,11 @@ dependencies { // Emoji implementation(libs.androidx.emoji.picker) - // hilt - implementation(libs.hilt.navigationCompose) - implementation(libs.hilt.work) - // smaller view models implementation(libs.resaca.core) - implementation(libs.resaca.hilt) + implementation(libs.resaca.metro) implementation(libs.bundlizer.core) + implementation(libs.dagger) allFlavors.forEach { flavor -> if (flavor in nonFreeFlavors) { @@ -331,9 +328,6 @@ dependencies { androidTestImplementation(libs.androidx.espresso.intents) androidTestImplementation(libs.androidx.espresso.accessibility) androidTestImplementation(libs.hamcrest) - androidTestImplementation(libs.hilt.test) - kspAndroidTest(libs.hilt.compiler) - androidTestImplementation(libs.androidx.test.extJunit) androidTestImplementation(libs.androidx.test.uiAutomator) androidTestImplementation(libs.androidx.test.work) diff --git a/app/src/androidTest/kotlin/com/wire/android/CustomTestRunner.kt b/app/src/androidTest/kotlin/com/wire/android/CustomTestRunner.kt index 67ab69e4b93..1d3dcecca95 100644 --- a/app/src/androidTest/kotlin/com/wire/android/CustomTestRunner.kt +++ b/app/src/androidTest/kotlin/com/wire/android/CustomTestRunner.kt @@ -23,6 +23,6 @@ import androidx.test.runner.AndroidJUnitRunner class CustomTestRunner : AndroidJUnitRunner() { override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { - return super.newApplication(cl, HiltTestApp_Application::class.java.name, context) + return super.newApplication(cl, WireApplication::class.java.name, context) } } diff --git a/app/src/androidTest/kotlin/com/wire/android/TestCoreLogicModule.kt b/app/src/androidTest/kotlin/com/wire/android/TestCoreLogicModule.kt deleted file mode 100644 index f8acaa6fd08..00000000000 --- a/app/src/androidTest/kotlin/com/wire/android/TestCoreLogicModule.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -package com.wire.android - -import android.content.Context -import androidx.work.WorkManager -import com.wire.android.di.CoreLogicModule -import com.wire.android.di.DefaultWebSocketEnabledByDefault -import com.wire.android.di.KaliumCoreLogic -import com.wire.android.di.NoSession -import com.wire.android.util.UserAgentProvider -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.id.QualifiedIdMapper -import com.wire.kalium.logic.feature.asset.AudioNormalizedLoudnessBuilder -import com.wire.kalium.logic.feature.server.ServerConfigForAccountUseCase -import com.wire.kalium.logic.feature.session.GetSessionsUseCase -import com.wire.kalium.logic.feature.session.ObserveSessionsUseCase -import com.wire.kalium.logic.feature.session.UpdateCurrentSessionUseCase -import com.wire.kalium.logic.featureFlags.KaliumConfigs -import com.wire.kalium.mocks.requests.ClientRequests -import com.wire.kalium.mocks.requests.FeatureConfigRequests -import com.wire.kalium.mocks.requests.LoginRequests -import com.wire.kalium.mocks.requests.NotificationRequests -import com.wire.kalium.network.NetworkStateObserver -import com.wire.kalium.network.utils.TestRequestHandler -import dagger.Module -import dagger.Provides -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import dagger.hilt.testing.TestInstallIn -import javax.inject.Singleton - -@Module -@TestInstallIn( - components = [SingletonComponent::class], - replaces = [CoreLogicModule::class], -) -class TestCoreLogicModule { - - @KaliumCoreLogic - @Singleton - @Provides - fun provideCoreLogic( - @ApplicationContext context: Context, - kaliumConfigs: KaliumConfigs, - userAgentProvider: UserAgentProvider - ): CoreLogic { - val rootPath = context.getDir("accounts", Context.MODE_PRIVATE).path - val mockedRequests = mutableListOf().apply { - addAll(LoginRequests.loginRequestResponseSuccess) - addAll(ClientRequests.clientRequestResponseSuccess) - addAll(FeatureConfigRequests.responseSuccess) - addAll(NotificationRequests.notificationsRequestResponseSuccess) - } - - return CoreLogic( - userAgent = userAgentProvider.defaultUserAgent, - appContext = context, - rootPath = rootPath, - kaliumConfigs = kaliumConfigs.copy( - mockedRequests = mockedRequests, - mockNetworkStateObserver = TestNetworkStateObserver.DEFAULT_TEST_NETWORK_STATE_OBSERVER - ) - ) - } - - @Singleton - @Provides - fun provideNetworkStateObserver(): NetworkStateObserver = TestNetworkStateObserver() - - @Provides - fun provideCurrentSessionUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().session.currentSession - - @Provides - fun deleteSessionUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().deleteSession - - @Provides - fun provideUpdateCurrentSessionUseCase(@KaliumCoreLogic coreLogic: CoreLogic): UpdateCurrentSessionUseCase = - coreLogic.getGlobalScope().session.updateCurrentSession - - @Provides - fun provideGetAllSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): GetSessionsUseCase = - coreLogic.getGlobalScope().session.allSessions - - @Provides - fun provideObserveAllSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ObserveSessionsUseCase = - coreLogic.getGlobalScope().session.allSessionsFlow - - @Provides - fun provideServerConfigForAccountUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ServerConfigForAccountUseCase = - coreLogic.getGlobalScope().serverConfigForAccounts - - @NoSession - @Singleton - @Provides - fun provideNoSessionQualifiedIdMapper(): QualifiedIdMapper = QualifiedIdMapper(null) - - @Singleton - @Provides - fun provideWorkManager(@ApplicationContext applicationContext: Context) = WorkManager.getInstance(applicationContext) - - @Provides - fun provideAudioNormalizedLoudnessBuilder(@KaliumCoreLogic coreLogic: CoreLogic): AudioNormalizedLoudnessBuilder = - coreLogic.audioNormalizedLoudnessBuilder - - @DefaultWebSocketEnabledByDefault - @Provides - fun provideDefaultWebSocketEnabledByDefault(): Boolean = true -} diff --git a/app/src/androidTest/kotlin/com/wire/android/WireActivityTest.kt b/app/src/androidTest/kotlin/com/wire/android/WireActivityTest.kt index c68cf09acd4..f36b31348e0 100644 --- a/app/src/androidTest/kotlin/com/wire/android/WireActivityTest.kt +++ b/app/src/androidTest/kotlin/com/wire/android/WireActivityTest.kt @@ -34,21 +34,15 @@ import com.wire.android.util.DataDogLogger import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.common.logger.CoreLogger -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test -@HiltAndroidTest class WireActivityTest { - @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) + @get:Rule val composeTestRule: AndroidComposeTestRule, WireActivity> = createAndroidComposeRule() @@ -58,7 +52,6 @@ class WireActivityTest { context.deleteDatabase("global-db") // GLOBAL_DB_NAME in FileNameUtil WorkManagerTestInitHelper.initializeTestWorkManager(context) initializeApplicationLoggingFrameworks() - hiltRule.inject() } @Ignore // TODO add other api mocks to not have flaky test diff --git a/app/src/foss/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt b/app/src/foss/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt index 793acb075fe..62e83cf9d89 100644 --- a/app/src/foss/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt +++ b/app/src/foss/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt @@ -17,9 +17,10 @@ */ package com.wire.android.ui.home.messagecomposer.location +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject -class LocationPickerHelperFlavor @Inject constructor( +class LocationPickerHelperFlavor @Inject @MetroInject constructor( private val locationPickerHelper: LocationPickerHelper, ) { suspend fun getLocation(onSuccess: (GeoLocatedAddress) -> Unit, onError: () -> Unit) { diff --git a/app/src/main/kotlin/com/wire/android/WireApplication.kt b/app/src/main/kotlin/com/wire/android/WireApplication.kt index fd875b93207..e309f8d2b43 100644 --- a/app/src/main/kotlin/com/wire/android/WireApplication.kt +++ b/app/src/main/kotlin/com/wire/android/WireApplication.kt @@ -28,32 +28,21 @@ import androidx.work.Configuration import androidx.work.WorkManager import co.touchlab.kermit.platformLogWriter import com.wire.android.analytics.ObserveCurrentSessionAnalyticsUseCase -import com.wire.android.datastore.GlobalDataStore -import com.wire.android.datastore.UserDataStoreProvider -import com.wire.android.di.ApplicationScope -import com.wire.android.di.KaliumCoreLogic -import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.feature.analytics.AnonymousAnalyticsRecorderImpl import com.wire.android.feature.analytics.globalAnalyticsManager import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.feature.analytics.model.AnalyticsSettings +import com.wire.android.di.metro.createWireMetroGraph import com.wire.android.util.AppNameUtil -import com.wire.android.util.CurrentScreenManager import com.wire.android.util.DataDogLogger -import com.wire.android.util.logging.LogFileWriter import com.wire.android.util.getGitBuildId -import com.wire.android.util.lifecycle.SyncLifecycleManager -import com.wire.android.workmanager.WireWorkerFactory import com.wire.android.workmanager.worker.enqueueAssetUploadObserver import com.wire.kalium.common.logger.CoreLogger import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.KaliumLogger -import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.GetAllSessionsResult -import dagger.Lazy -import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine @@ -65,51 +54,27 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout -import javax.inject.Inject import kotlin.collections.filter @Suppress("TooManyFunctions") -@HiltAndroidApp class WireApplication : BaseApp() { - @Inject - @KaliumCoreLogic - lateinit var coreLogic: Lazy - - @Inject - lateinit var logFileWriter: Lazy - - @Inject - lateinit var syncLifecycleManager: Lazy - - @Inject - lateinit var wireWorkerFactory: Lazy - - @Inject - lateinit var globalObserversManager: Lazy - - @Inject - lateinit var globalDataStore: Lazy - - @Inject - lateinit var userDataStoreProvider: Lazy - - @Inject - @ApplicationScope - lateinit var globalAppScope: CoroutineScope - - @Inject - lateinit var currentScreenManager: CurrentScreenManager - - @Inject - lateinit var analyticsManager: Lazy - - @Inject - lateinit var workManager: WorkManager + private val metroGraph by lazy { createWireMetroGraph(this) } + private val coreLogic by lazy { metroGraph.coreLogic } + private val logFileWriter by lazy { metroGraph.logFileWriter } + private val syncLifecycleManager by lazy { metroGraph.syncLifecycleManager } + private val wireWorkerFactory by lazy { metroGraph.wireWorkerFactory } + private val globalObserversManager by lazy { metroGraph.globalObserversManager } + private val globalDataStore by lazy { metroGraph.globalDataStore } + private val userDataStoreProvider by lazy { metroGraph.userDataStoreProvider } + private val globalAppScope: CoroutineScope by lazy { metroGraph.applicationScope } + private val currentScreenManager by lazy { metroGraph.currentScreenManager } + private val analyticsManager by lazy { metroGraph.analyticsManager } + private val workManager: WorkManager by lazy { metroGraph.workManager } override val workManagerConfiguration: Configuration get() = Configuration.Builder() - .setWorkerFactory(wireWorkerFactory.get()) + .setWorkerFactory(wireWorkerFactory) .setMinimumLoggingLevel(android.util.Log.DEBUG) .build() @@ -130,11 +95,11 @@ class WireApplication : BaseApp() { ProcessLifecycleOwner.get().lifecycle.addObserver(currentScreenManager) } launch { - syncLifecycleManager.get().observeAppLifecycle() + syncLifecycleManager.observeAppLifecycle() } appLogger.i("$TAG global observers") - globalObserversManager.get().observe() + globalObserversManager.observe() launch { observeAssetUploadState() } @@ -147,34 +112,34 @@ class WireApplication : BaseApp() { private suspend fun observeCallBackgroundState() { combine( currentScreenManager.isAppVisibleFlow(), - coreLogic.get().getGlobalScope().session.allSessionsFlow() + coreLogic.getGlobalScope().session.allSessionsFlow() .filterIsInstance() .map { it.sessions.filter { it.isValid() } }, ::Pair ).collect { (isAppVisible, validSessions) -> validSessions.forEach { - coreLogic.get().getSessionScope(it.userId).calls.setBackground(!isAppVisible) + coreLogic.getSessionScope(it.userId).calls.setBackground(!isAppVisible) } } } private suspend fun observeRecentlyEndedCall() { - coreLogic.get().getGlobalScope().session.currentSessionFlow().filterIsInstance(CurrentSessionResult.Success::class) + coreLogic.getGlobalScope().session.currentSessionFlow().filterIsInstance(CurrentSessionResult.Success::class) .filter { session -> session.accountInfo.isValid() } .flatMapLatest { session -> - coreLogic.get().getSessionScope(session.accountInfo.userId).calls.observeRecentlyEndedCallMetadata() + coreLogic.getSessionScope(session.accountInfo.userId).calls.observeRecentlyEndedCallMetadata() } .collect { metadata -> - analyticsManager.get().sendEvent(AnalyticsEvent.RecentlyEndedCallEvent(metadata)) + analyticsManager.sendEvent(AnalyticsEvent.RecentlyEndedCallEvent(metadata)) } } private suspend fun observeAssetUploadState() { - coreLogic.get().getGlobalScope().session.currentSessionFlow() + coreLogic.getGlobalScope().session.currentSessionFlow() .filterIsInstance() .map { it.accountInfo.userId } .flatMapLatest { - coreLogic.get().getSessionScope(it).messages.observeAssetUploadState() + coreLogic.getSessionScope(it).messages.observeAssetUploadState() } .collect { uploadInProgress -> if (uploadInProgress) { @@ -228,7 +193,7 @@ class WireApplication : BaseApp() { try { // Use a very short timeout to avoid delaying the crash withTimeout(CRASH_FLUSH_TIMEOUT_MS) { - logFileWriter.get().forceFlush() + logFileWriter.forceFlush() } appLogger.i("Logs flushed before crash") } catch (e: Exception) { @@ -304,7 +269,7 @@ class WireApplication : BaseApp() { // 1. Datadog should be initialized first ExternalLoggerManager.initDatadogLogger(applicationContext) // 2. Initialize our internal logging framework - val isLoggingEnabled = globalDataStore.get().isLoggingEnabled().first() + val isLoggingEnabled = globalDataStore.isLoggingEnabled().first() val config = if (isLoggingEnabled) { KaliumLogger.Config( KaliumLogLevel.VERBOSE, @@ -317,7 +282,7 @@ class WireApplication : BaseApp() { AppLogger.init(config) CoreLogger.init(config) // 3. Initialize our internal FILE logging framework - logFileWriter.get().start() + logFileWriter.start() // 4. Everything ready, now we can log device info appLogger.i("Logger enabled") logDeviceInformation() @@ -336,20 +301,20 @@ class WireApplication : BaseApp() { ) val analyticsResultFlow = ObserveCurrentSessionAnalyticsUseCase( - currentSessionFlow = coreLogic.get().getGlobalScope().session.currentSessionFlow(), + currentSessionFlow = coreLogic.getGlobalScope().session.currentSessionFlow(), getAnalyticsContactsData = { userId -> - coreLogic.get().getSessionScope(userId).getAnalyticsContactsData() + coreLogic.getSessionScope(userId).getAnalyticsContactsData() }, observeAnalyticsTrackingIdentifierStatusFlow = { userId -> - coreLogic.get().getSessionScope(userId).observeAnalyticsTrackingIdentifierStatus() + coreLogic.getSessionScope(userId).observeAnalyticsTrackingIdentifierStatus() }, analyticsIdentifierManagerProvider = { userId -> - coreLogic.get().getSessionScope(userId).analyticsIdentifierManager + coreLogic.getSessionScope(userId).analyticsIdentifierManager }, - userDataStoreProvider = userDataStoreProvider.get(), - globalDataStore = globalDataStore.get(), + userDataStoreProvider = userDataStoreProvider, + globalDataStore = globalDataStore, currentBackend = { userId -> - coreLogic.get().getSessionScope(userId).users.serverLinks() + coreLogic.getSessionScope(userId).users.serverLinks() } ).invoke() @@ -374,9 +339,9 @@ class WireApplication : BaseApp() { .isAppVisibleFlow() .filter { isVisible -> isVisible } .collect { - val currentSessionResult = coreLogic.get().getGlobalScope().session.currentSessionFlow().first() + val currentSessionResult = coreLogic.getGlobalScope().session.currentSessionFlow().first() val isTeamMember = if (currentSessionResult is CurrentSessionResult.Success) { - coreLogic.get().getSessionScope(currentSessionResult.accountInfo.userId).team.isSelfATeamMember() + coreLogic.getSessionScope(currentSessionResult.accountInfo.userId).team.isSelfATeamMember() } else { null } @@ -412,7 +377,7 @@ class WireApplication : BaseApp() { super.onLowMemory() appLogger.w("onLowMemory called - Stopping logging, buckling the seatbelt and hoping for the best!") globalAppScope.launch { - logFileWriter.get().stop() + logFileWriter.stop() } } diff --git a/app/src/main/kotlin/com/wire/android/analytics/FinalizeRegistrationAnalyticsMetadataUseCase.kt b/app/src/main/kotlin/com/wire/android/analytics/FinalizeRegistrationAnalyticsMetadataUseCase.kt index fdf3703eb10..f801a4521ae 100644 --- a/app/src/main/kotlin/com/wire/android/analytics/FinalizeRegistrationAnalyticsMetadataUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/analytics/FinalizeRegistrationAnalyticsMetadataUseCase.kt @@ -23,7 +23,7 @@ import com.wire.android.di.KaliumCoreLogic import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.user.UserId import kotlinx.coroutines.flow.firstOrNull -import javax.inject.Inject +import dev.zacsweers.metro.Inject /** * Finalize the registration process and analytics metadata in case there was enabled in the process. diff --git a/app/src/main/kotlin/com/wire/android/analytics/RegistrationAnalyticsManagerUseCase.kt b/app/src/main/kotlin/com/wire/android/analytics/RegistrationAnalyticsManagerUseCase.kt index e827c836253..c7c8bbc9ed9 100644 --- a/app/src/main/kotlin/com/wire/android/analytics/RegistrationAnalyticsManagerUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/analytics/RegistrationAnalyticsManagerUseCase.kt @@ -21,7 +21,7 @@ import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.feature.analytics.model.AnalyticsEvent import kotlinx.coroutines.flow.firstOrNull -import javax.inject.Inject +import dev.zacsweers.metro.Inject class RegistrationAnalyticsManagerUseCase @Inject constructor( private val globalDataStore: GlobalDataStore, diff --git a/app/src/main/kotlin/com/wire/android/config/NomadProfilesFeatureConfig.kt b/app/src/main/kotlin/com/wire/android/config/NomadProfilesFeatureConfig.kt index e40da13b75a..d77e8a38bfa 100644 --- a/app/src/main/kotlin/com/wire/android/config/NomadProfilesFeatureConfig.kt +++ b/app/src/main/kotlin/com/wire/android/config/NomadProfilesFeatureConfig.kt @@ -19,7 +19,7 @@ package com.wire.android.config import com.wire.android.BuildConfig -import javax.inject.Inject +import dev.zacsweers.metro.Inject class NomadProfilesFeatureConfig @Inject constructor() { fun isEnabled(): Boolean = BuildConfig.NOMAD_PROFILES_ENABLED diff --git a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt index b81e777bab6..4ca3984f83e 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt @@ -30,7 +30,7 @@ import com.wire.android.BuildConfig import com.wire.android.feature.AppLockSource import com.wire.android.ui.theme.ThemeOption import com.wire.android.util.sha256 -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull diff --git a/app/src/main/kotlin/com/wire/android/datastore/UserDataStoreProvider.kt b/app/src/main/kotlin/com/wire/android/datastore/UserDataStoreProvider.kt index c9d85b4208a..0cc2aec5e13 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/UserDataStoreProvider.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/UserDataStoreProvider.kt @@ -20,7 +20,7 @@ package com.wire.android.datastore import android.content.Context import com.wire.kalium.logic.data.user.UserId -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap import javax.inject.Inject diff --git a/app/src/main/kotlin/com/wire/android/di/AppModule.kt b/app/src/main/kotlin/com/wire/android/di/AppModule.kt index 932a3d00e94..17ca111f222 100644 --- a/app/src/main/kotlin/com/wire/android/di/AppModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/AppModule.kt @@ -17,115 +17,3 @@ */ package com.wire.android.di - -import android.app.NotificationManager -import android.content.Context -import android.location.Geocoder -import android.media.AudioAttributes -import android.media.AudioManager -import android.media.MediaPlayer -import androidx.core.app.NotificationManagerCompat -import com.wire.android.BuildConfig -import com.wire.android.feature.analytics.AnonymousAnalyticsManager -import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl -import com.wire.android.mapper.MessageResourceProvider -import com.wire.android.ui.analytics.AnalyticsConfiguration -import com.wire.android.ui.home.appLock.CurrentTimestampProvider -import com.wire.android.ui.home.conversations.MessageSharedState -import com.wire.android.ui.home.messagecomposer.location.LocationPickerParameters -import com.wire.android.util.GetMediaMetadataUseCase -import com.wire.android.util.GetMediaMetadataUseCaseImpl -import com.wire.android.util.dispatchers.DefaultDispatcherProvider -import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.ui.AndroidUiTextResolver -import com.wire.android.util.ui.UiTextResolver -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Named -import javax.inject.Qualifier -import javax.inject.Singleton - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class CurrentAppVersion - -@Module -@InstallIn(SingletonComponent::class) -@Suppress("TooManyFunctions") -object AppModule { - - @CurrentAppVersion - @Provides - fun provideCurrentAppVersion(): Int = BuildConfig.VERSION_CODE - - @Singleton - @Provides - fun providesApplicationContext(@ApplicationContext appContext: Context) = appContext - - @Singleton - @Provides - fun provideDefaultDispatchers(): DispatcherProvider = DefaultDispatcherProvider() - - @Provides - fun provideMessageResourceProvider(): MessageResourceProvider = MessageResourceProvider() - - @Singleton - @Provides - fun provideUiTextResolver(@ApplicationContext appContext: Context): UiTextResolver = - AndroidUiTextResolver(appContext) - - @Provides - fun provideNotificationManagerCompat(appContext: Context): NotificationManagerCompat = - NotificationManagerCompat.from(appContext) - - @Provides - fun provideNotificationManager(appContext: Context): NotificationManager = - appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - @Provides - fun provideMusicMediaPlayer(): MediaPlayer { - return MediaPlayer().apply { - setAudioAttributes( - AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .setUsage(AudioAttributes.USAGE_MEDIA) - .build() - ) - } - } - - @Singleton - @Provides - fun provideCurrentTimestampProvider(): CurrentTimestampProvider = { System.currentTimeMillis() } - - @Provides - fun provideGeocoder(appContext: Context): Geocoder = Geocoder(appContext) - - @Provides - fun provideLocationPickerParameters(): LocationPickerParameters = LocationPickerParameters() - - @Provides - fun provideAnalyticsConfiguration() = - if (BuildConfig.ANALYTICS_ENABLED) AnalyticsConfiguration.Enabled else AnalyticsConfiguration.Disabled - - @Provides - fun provideAnonymousAnalyticsManager(): AnonymousAnalyticsManager = AnonymousAnalyticsManagerImpl - - @Provides - fun provideAudioManager(@ApplicationContext context: Context): AudioManager = - context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - - @Provides - @Named("useNewLoginForDefaultBackend") - fun provideUseNewLoginForDefaultBackend(): Boolean = BuildConfig.USE_NEW_LOGIN_FOR_DEFAULT_BACKEND - - @Provides - @Singleton - fun provideMessageSharedState(): MessageSharedState = MessageSharedState() - - @Provides - fun provideGetMediaMetadataUseCase(): GetMediaMetadataUseCase = GetMediaMetadataUseCaseImpl() -} diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index 7a8550acec6..58ab864b25a 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -18,63 +18,7 @@ package com.wire.android.di -import android.content.Context -import androidx.work.WorkManager -import com.wire.android.datastore.UserDataStoreProvider -import com.wire.android.emm.ManagedConfigurationsManager -import com.wire.android.util.ImageUtil -import com.wire.android.util.UserAgentProvider -import com.wire.android.util.isWebsocketEnabledByDefault -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.asset.KaliumFileSystem -import com.wire.kalium.logic.data.id.FederatedIdMapper -import com.wire.kalium.logic.data.id.QualifiedIdMapper -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.analytics.GetCurrentAnalyticsTrackingIdentifierUseCase -import com.wire.kalium.logic.feature.asset.AudioNormalizedLoudnessBuilder -import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase -import com.wire.kalium.logic.feature.auth.LogoutUseCase -import com.wire.kalium.logic.feature.auth.sso.ValidateSSOCodeUseCase -import com.wire.kalium.logic.feature.connection.BlockUserUseCase -import com.wire.kalium.logic.feature.connection.UnblockUserUseCase -import com.wire.kalium.logic.feature.conversation.ObserveOtherUserSecurityClassificationLabelUseCase -import com.wire.kalium.logic.feature.conversation.ObserveSecurityClassificationLabelUseCase -import com.wire.kalium.logic.feature.e2ei.usecase.FetchConversationMLSVerificationStatusUseCase -import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase -import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase -import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveTeamSettingsSelfDeletingStatusUseCase -import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase -import com.wire.kalium.logic.feature.server.ServerConfigForAccountUseCase -import com.wire.kalium.logic.feature.session.CurrentSessionResult -import com.wire.kalium.logic.feature.session.DoesValidNomadAccountExistUseCase -import com.wire.kalium.logic.feature.session.DoesValidSessionExistUseCase -import com.wire.kalium.logic.feature.session.GetSessionsUseCase -import com.wire.kalium.logic.feature.session.ObserveSessionsUseCase -import com.wire.kalium.logic.feature.session.UpdateCurrentSessionUseCase -import com.wire.kalium.logic.feature.user.MarkFileSharingChangeAsNotifiedUseCase -import com.wire.kalium.logic.feature.user.MarkSelfDeletionStatusAsNotifiedUseCase -import com.wire.kalium.logic.feature.user.ObserveValidAccountsUseCase -import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase -import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCase -import com.wire.kalium.logic.featureFlags.KaliumConfigs -import com.wire.kalium.logic.util.RandomPassword -import com.wire.kalium.network.NetworkStateObserver -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ServiceComponent -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.android.scopes.ServiceScoped -import dagger.hilt.android.scopes.ViewModelScoped -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.runBlocking import javax.inject.Qualifier -import javax.inject.Singleton - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class CurrentSessionFlowService @Qualifier @Retention(AnnotationRetention.BINARY) @@ -87,428 +31,3 @@ annotation class NoSession @Qualifier @Retention(AnnotationRetention.BINARY) annotation class DefaultWebSocketEnabledByDefault - -@Module -@InstallIn(SingletonComponent::class) -class CoreLogicModule { - - @KaliumCoreLogic - @Singleton - @Provides - fun provideCoreLogic( - @ApplicationContext context: Context, - kaliumConfigs: KaliumConfigs, - userAgentProvider: UserAgentProvider - ): CoreLogic { - val rootPath = context.getDir("accounts", Context.MODE_PRIVATE).path - - return CoreLogic( - userAgent = userAgentProvider.defaultUserAgent, - appContext = context, - rootPath = rootPath, - kaliumConfigs = kaliumConfigs - ) - } - - @Singleton - @Provides - fun provideNetworkStateObserver(@KaliumCoreLogic coreLogic: CoreLogic): NetworkStateObserver = - coreLogic.networkStateObserver - - @Provides - fun provideCurrentSessionUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().session.currentSession - - @Provides - fun deleteSessionUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().deleteSession - - @Provides - fun provideUpdateCurrentSessionUseCase(@KaliumCoreLogic coreLogic: CoreLogic): UpdateCurrentSessionUseCase = - coreLogic.getGlobalScope().session.updateCurrentSession - - @Provides - fun provideGetAllSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): GetSessionsUseCase = - coreLogic.getGlobalScope().session.allSessions - - @Provides - fun provideObserveAllSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ObserveSessionsUseCase = - coreLogic.getGlobalScope().session.allSessionsFlow - - @Provides - fun provideServerConfigForAccountUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ServerConfigForAccountUseCase = - coreLogic.getGlobalScope().serverConfigForAccounts - - @NoSession - @Singleton - @Provides - fun provideNoSessionQualifiedIdMapper(): QualifiedIdMapper = QualifiedIdMapper(null) - - @Singleton - @Provides - fun provideWorkManager(@ApplicationContext applicationContext: Context) = WorkManager.getInstance(applicationContext) - - @Provides - fun provideAudioNormalizedLoudnessBuilder(@KaliumCoreLogic coreLogic: CoreLogic): AudioNormalizedLoudnessBuilder = - coreLogic.audioNormalizedLoudnessBuilder - - @DefaultWebSocketEnabledByDefault - @Provides - fun provideDefaultWebSocketEnabledByDefault( - @ApplicationContext context: Context, - managedConfigurationsManager: ManagedConfigurationsManager - ): Boolean = isWebsocketEnabledByDefault( - context, - managedConfigurationsManager.persistentWebSocketEnforcedByMDM.value - ) -} - -@Module -@InstallIn(ViewModelComponent::class) -class SessionModule { - // TODO: can be improved by caching the current session in kalium or changing the scope to ActivityRetainedScoped - @CurrentAccount - @ViewModelScoped - @Provides - fun provideCurrentSession(@KaliumCoreLogic coreLogic: CoreLogic): UserId { - return runBlocking { - return@runBlocking when (val result = coreLogic.getGlobalScope().session.currentSession.invoke()) { - is CurrentSessionResult.Success -> result.accountInfo.userId - else -> { - throw IllegalStateException("no current session was found") - } - } - } - } - - @ViewModelScoped - @Provides - fun provideCurrentAccountUserDataStore(@CurrentAccount currentAccount: UserId, userDataStoreProvider: UserDataStoreProvider) = - userDataStoreProvider.getOrCreate(currentAccount) -} - -@Module -@InstallIn(ServiceComponent::class) -class ServiceModule { - @ServiceScoped - @Provides - @CurrentSessionFlowService - fun provideCurrentSessionFlowUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().session.currentSessionFlow -} - -@Module -@InstallIn(ViewModelComponent::class) -@Suppress("TooManyFunctions", "LargeClass") -class UseCaseModule { - - @ViewModelScoped - @Provides - fun provideObserveSyncStateUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).observeSyncState - - @ViewModelScoped - @Provides - fun provideLogoutUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId): LogoutUseCase = - coreLogic.getSessionScope(currentAccount).logout - - @Provides - fun provideValidateEmailUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().validateEmailUseCase - - @Provides - fun provideValidateSSOCodeUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ValidateSSOCodeUseCase = - coreLogic.getGlobalScope().validateSSOCodeUseCase - - @ViewModelScoped - @Provides - fun provideValidatePasswordUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().validatePasswordUseCase - - @ViewModelScoped - @Provides - fun provideValidateUserHandleUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().validateUserHandleUseCase - - @ViewModelScoped - @Provides - fun provideGetServerConfigUserCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().fetchServerConfigFromDeepLink - - @ViewModelScoped - @Provides - fun provideCurrentSessionFlowUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().session.currentSessionFlow - - @ViewModelScoped - @Provides - fun provideAddAuthenticatedUserUseCase(@KaliumCoreLogic coreLogic: CoreLogic): AddAuthenticatedUserUseCase = - coreLogic.getGlobalScope().addAuthenticatedAccount - - @ViewModelScoped - @Provides - fun provideObservePersistentWebSocketConnectionStatusUseCase( - @KaliumCoreLogic coreLogic: CoreLogic - ) = coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus - - @ViewModelScoped - @Provides - fun providePersistPersistentWebSocketConnectionStatusUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ) = coreLogic.getSessionScope(currentAccount).persistPersistentWebSocketConnectionStatus - - @ViewModelScoped - @Provides - fun provideGetPersistentWebSocketStatusUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ) = coreLogic.getSessionScope(currentAccount).getPersistentWebSocketStatus - - @ViewModelScoped - @Provides - fun provideCheckCrlRevocationListUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).checkCrlRevocationList - - @ViewModelScoped - @Provides - fun provideIsMLSEnabledUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).isMLSEnabled - - @ViewModelScoped - @Provides - fun provideGetDefaultProtocol(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).getDefaultProtocol - - @ViewModelScoped - @Provides - fun provideIsE2EIEnabledUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).isE2EIEnabled - - @ViewModelScoped - @Provides - fun provideIsFileSharingEnabledUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).isFileSharingEnabled - - @ViewModelScoped - @Provides - fun provideFileSharingStatusFlowUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).observeFileSharingStatus - - @ViewModelScoped - @Provides - fun fileSystemProvider(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId): KaliumFileSystem = - coreLogic.getSessionScope(currentAccount).kaliumFileSystem - - @ViewModelScoped - @Provides - fun provideFederatedIdMapper( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): FederatedIdMapper = - coreLogic.getSessionScope(currentAccount).federatedIdMapper - - @ViewModelScoped - @Provides - fun provideQualifiedIdMapper( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): QualifiedIdMapper = - coreLogic.getSessionScope(currentAccount).qualifiedIdMapper - - @ViewModelScoped - @Provides - fun provideBlockUserUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): BlockUserUseCase = coreLogic.getSessionScope(currentAccount).connection.blockUser - - @ViewModelScoped - @Provides - fun provideUnblockUserUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): UnblockUserUseCase = coreLogic.getSessionScope(currentAccount).connection.unblockUser - - @ViewModelScoped - @Provides - fun provideObserveValidAccountsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ObserveValidAccountsUseCase = - coreLogic.getGlobalScope().observeValidAccounts - - @ViewModelScoped - @Provides - fun provideDoesValidSessionExistsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): DoesValidSessionExistUseCase = - coreLogic.getGlobalScope().doesValidSessionExist - - @ViewModelScoped - @Provides - fun provideDoesValidNomadAccountExistUseCase(@KaliumCoreLogic coreLogic: CoreLogic): DoesValidNomadAccountExistUseCase = - coreLogic.getGlobalScope().doesValidNomadAccountExist - - @ViewModelScoped - @Provides - fun observeSecurityClassificationLabelUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): ObserveSecurityClassificationLabelUseCase = - coreLogic.getSessionScope(currentAccount).observeSecurityClassificationLabel - - @ViewModelScoped - @Provides - fun provideCreateMpBackupUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).multiPlatformBackup.create - - @ViewModelScoped - @Provides - fun provideRestoreMpBackupUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).multiPlatformBackup.restore - - @ViewModelScoped - @Provides - fun provideUpdateApiVersionsScheduler(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().updateApiVersionsScheduler - - @ViewModelScoped - @Provides - fun provideObserveIfAppFreshEnoughUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().observeIfAppUpdateRequired - - @ViewModelScoped - @Provides - fun provideMarkFileSharingStatusAsNotified( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): MarkFileSharingChangeAsNotifiedUseCase = coreLogic.getSessionScope(currentAccount).markFileSharingStatusAsNotified - - @ViewModelScoped - @Provides - fun provideMarkSelfDeletingMessagesAsNotified( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): MarkSelfDeletionStatusAsNotifiedUseCase = coreLogic.getSessionScope(currentAccount).markSelfDeletingMessagesAsNotified - - @ViewModelScoped - @Provides - fun provideObserveTeamSettingsSelfDeletionStatusFlagUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): ObserveTeamSettingsSelfDeletingStatusUseCase = coreLogic.getSessionScope(currentAccount).observeTeamSettingsSelfDeletionStatus - - @ViewModelScoped - @Provides - fun provideObserveSelfDeletionTimerSettingsForConversationUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): ObserveSelfDeletionTimerSettingsForConversationUseCase = coreLogic.getSessionScope(currentAccount).observeSelfDeletingMessages - - @ViewModelScoped - @Provides - fun providePersistNewSelfDeletingMessagesUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): PersistNewSelfDeletionTimerUseCase = coreLogic.getSessionScope(currentAccount).persistNewSelfDeletionStatus - - @ViewModelScoped - @Provides - fun provideImageUtil(): ImageUtil = ImageUtil - - @ViewModelScoped - @Provides - fun provideObserveGuestRoomLinkFeatureFlagUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).observeGuestRoomLinkFeatureFlag - - @ViewModelScoped - @Provides - fun provideMarkGuestLinkFeatureFlagAsNotChangedUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).markGuestLinkFeatureFlagAsNotChanged - - @ViewModelScoped - @Provides - fun provideMarkTeamAppLockStatusAsNotifiedUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).markTeamAppLockStatusAsNotified - - @ViewModelScoped - @Provides - fun provideGetOtherUserSecurityClassificationLabelUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): ObserveOtherUserSecurityClassificationLabelUseCase = - coreLogic.getSessionScope(currentAccount).getOtherUserSecurityClassificationLabel - - @ViewModelScoped - @Provides - fun provideObserveNewClientsUseCaseUseCase(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().observeNewClientsUseCase - - @ViewModelScoped - @Provides - fun provideClearNewClientsForUser(@KaliumCoreLogic coreLogic: CoreLogic) = - coreLogic.getGlobalScope().clearNewClientsForUser - - @ViewModelScoped - @Provides - fun providePersistScreenshotCensoringConfigUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): PersistScreenshotCensoringConfigUseCase = coreLogic.getSessionScope(currentAccount).persistScreenshotCensoringConfig - - @ViewModelScoped - @Provides - fun provideObserveScreenshotCensoringConfigUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): ObserveScreenshotCensoringConfigUseCase = coreLogic.getSessionScope(currentAccount).observeScreenshotCensoringConfig - - @ViewModelScoped - @Provides - fun provideObserveIsAppLockEditableUseCase( - @KaliumCoreLogic coreLogic: CoreLogic - ): ObserveIsAppLockEditableUseCase = coreLogic.getGlobalScope().observeIsAppLockEditableUseCase - - @ViewModelScoped - @Provides - fun provideObserveLegalHoldRequestUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).observeLegalHoldRequest - - @ViewModelScoped - @Provides - fun provideObserveLegalHoldForSelfUserUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).observeLegalHoldForSelfUser - - @ViewModelScoped - @Provides - fun provideObserveLegalHoldForUserUseCase(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId) = - coreLogic.getSessionScope(currentAccount).observeLegalHoldStateForUser - - @ViewModelScoped - @Provides - fun provideFetchConversationMLSVerificationStatusUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): FetchConversationMLSVerificationStatusUseCase = coreLogic.getSessionScope(currentAccount).fetchConversationMLSVerificationStatus - - @ViewModelScoped - @Provides - fun provideGetCurrentAnalyticsTrackingIdentifierUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): GetCurrentAnalyticsTrackingIdentifierUseCase = coreLogic.getSessionScope(currentAccount).getCurrentAnalyticsTrackingIdentifier - - @ViewModelScoped - @Provides - fun provideMigrateFromPersonalToTeamUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ) = coreLogic.getSessionScope(currentAccount).migrateFromPersonalToTeam - - @ViewModelScoped - @Provides - fun provideGetTeamUrlUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ) = coreLogic.getSessionScope(currentAccount).getTeamUrlUseCase - - @ViewModelScoped - @Provides - fun provideGenerateRandomPasswordUseCase() = RandomPassword() -} diff --git a/app/src/main/kotlin/com/wire/android/di/CoroutineScope.kt b/app/src/main/kotlin/com/wire/android/di/CoroutineScope.kt index 6e1f93790ee..049cbaac5fa 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoroutineScope.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoroutineScope.kt @@ -18,16 +18,7 @@ package com.wire.android.di -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob import javax.inject.Qualifier -import javax.inject.Singleton @Retention(AnnotationRetention.RUNTIME) @Qualifier @@ -48,36 +39,3 @@ annotation class MainDispatcher @Retention(AnnotationRetention.BINARY) @Qualifier annotation class MainImmediateDispatcher - -@InstallIn(SingletonComponent::class) -@Module -object CoroutinesScopesModule { - - @Singleton - @ApplicationScope - @Provides - fun providesCoroutineScope( - @DefaultDispatcher defaultDispatcher: CoroutineDispatcher - ): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher) -} - -@InstallIn(SingletonComponent::class) -@Module -object CoroutinesDispatchersModule { - - @DefaultDispatcher - @Provides - fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default - - @IoDispatcher - @Provides - fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO - - @MainDispatcher - @Provides - fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main - - @MainImmediateDispatcher - @Provides - fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate -} diff --git a/app/src/main/kotlin/com/wire/android/di/ImageLoadingModule.kt b/app/src/main/kotlin/com/wire/android/di/ImageLoadingModule.kt index 6a0d5c6ba86..17ca111f222 100644 --- a/app/src/main/kotlin/com/wire/android/di/ImageLoadingModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/ImageLoadingModule.kt @@ -17,43 +17,3 @@ */ package com.wire.android.di - -import android.content.Context -import com.wire.android.util.ui.WireSessionImageLoader -import com.wire.kalium.logic.feature.asset.DeleteAssetUseCase -import com.wire.kalium.logic.feature.asset.GetAvatarAssetUseCase -import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase -import com.wire.kalium.network.NetworkStateObserver -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.qualifiers.ApplicationContext - -/** - * Module that holds everything necessary to load images. - * It's installed in [ViewModelComponent] as it is something that depends on the currently active user session - */ -@Module -@InstallIn(ViewModelComponent::class) -class ImageLoadingModule { - - @Provides - fun provideImageLoaderFactory( - @ApplicationContext context: Context, - getAvatarAsset: GetAvatarAssetUseCase, - deleteAsset: DeleteAssetUseCase, - getMessageAsset: GetMessageAssetUseCase, - networkStateObserver: NetworkStateObserver, - ): WireSessionImageLoader.Factory = WireSessionImageLoader.Factory( - context = context, - getAvatarAsset = getAvatarAsset, - deleteAsset = deleteAsset, - networkStateObserver = networkStateObserver, - getPrivateAsset = getMessageAsset - ) - - // For better performance/caching. We shouldn't create many of these ImageLoaders. - @Provides - fun provideWireImageLoader(imageLoaderFactory: WireSessionImageLoader.Factory) = imageLoaderFactory.newImageLoader() -} diff --git a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt index 10bba57fe7e..17ca111f222 100644 --- a/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/KaliumConfigsModule.kt @@ -17,52 +17,3 @@ */ package com.wire.android.di - -import android.os.Build -import com.wire.android.BuildConfig -import com.wire.kalium.logic.featureFlags.BuildFileRestrictionState -import com.wire.kalium.logic.featureFlags.KaliumConfigs -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -class KaliumConfigsModule { - - @Provides - fun provideKaliumConfigs(): KaliumConfigs { - return KaliumConfigs( - fileRestrictionState = lazy { - if (BuildConfig.FILE_RESTRICTION_ENABLED) { - BuildConfig.FILE_RESTRICTION_LIST.split(",").map { it.trim() }.let { - BuildFileRestrictionState.AllowSome(it) - } - } else { - BuildFileRestrictionState.NoRestriction - } - }, - forceConstantBitrateCalls = BuildConfig.FORCE_CONSTANT_BITRATE_CALLS, - // we use upsert, available from SQL3.24, which is supported from Android API30, so for older APIs we have to use SQLCipher - shouldEncryptData = { !BuildConfig.DEBUG || Build.VERSION.SDK_INT < Build.VERSION_CODES.R }, - lowerKeyPackageLimits = BuildConfig.LOWER_KEYPACKAGE_LIMIT, - developmentApiEnabled = BuildConfig.DEVELOPMENT_API_ENABLED, - ignoreSSLCertificatesForUnboundCalls = BuildConfig.IGNORE_SSL_CERTIFICATES, - encryptProteusStorage = true, - guestRoomLink = BuildConfig.ENABLE_GUEST_ROOM_LINK, - selfDeletingMessages = BuildConfig.SELF_DELETING_MESSAGES, - wipeOnCookieInvalid = BuildConfig.WIPE_ON_COOKIE_INVALID, - wipeOnDeviceRemoval = BuildConfig.WIPE_ON_DEVICE_REMOVAL, - wipeOnRootedDevice = BuildConfig.WIPE_ON_ROOTED_DEVICE, - certPinningConfig = BuildConfig.CERTIFICATE_PINNING_CONFIG, - maxRemoteSearchResultCount = BuildConfig.MAX_REMOTE_SEARCH_RESULT_COUNT, - limitTeamMembersFetchDuringSlowSync = BuildConfig.LIMIT_TEAM_MEMBERS_FETCH_DURING_SLOW_SYNC, - isMlsResetEnabled = BuildConfig.IS_MLS_RESET_ENABLED, - collaboraIntegration = BuildConfig.COLLABORA_INTEGRATION_ENABLED, - dbInvalidationControlEnabled = BuildConfig.DB_INVALIDATION_CONTROL_ENABLED, - domainWithFaultyKeysMap = BuildConfig.DOMAIN_REMOVAL_KEYS_FOR_REPAIR, - isDebug = BuildConfig.DEBUG - ) - } -} diff --git a/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt b/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt index f90c9093484..17ca111f222 100644 --- a/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt @@ -17,32 +17,3 @@ */ package com.wire.android.di - -import android.content.Context -import com.wire.android.BuildConfig -import com.wire.android.util.logging.LogFileWriterV1Impl - import com.wire.android.util.logging.LogFileWriter -import com.wire.android.util.logging.LogFileWriterV2Impl -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class LogWriterModule { - - @Singleton - @Provides - fun provideKaliumFileWriter(@ApplicationContext context: Context): LogFileWriter { - if (BuildConfig.USE_ASYNC_FLUSH_LOGGING) { - val logsDirectory = LogFileWriter.logsDirectory(context) - return LogFileWriterV2Impl(logsDirectory) - } else { - val logsDirectory = LogFileWriter.logsDirectory(context) - return LogFileWriterV1Impl(logsDirectory) - } - } -} diff --git a/app/src/main/kotlin/com/wire/android/di/ManagedConfigurationsModule.kt b/app/src/main/kotlin/com/wire/android/di/ManagedConfigurationsModule.kt index ab1fd85f372..632b2074f3c 100644 --- a/app/src/main/kotlin/com/wire/android/di/ManagedConfigurationsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/ManagedConfigurationsModule.kt @@ -16,89 +16,3 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.wire.android.di - -import android.content.Context -import com.wire.android.BuildConfig -import com.wire.android.config.ServerConfigProvider -import com.wire.android.datastore.GlobalDataStore -import com.wire.android.emm.AndroidUserContextProvider -import com.wire.android.emm.AndroidUserContextProviderImpl -import com.wire.android.emm.ManagedConfigParser -import com.wire.android.emm.ManagedConfigParserImpl -import com.wire.android.emm.ManagedConfigurationsManager -import com.wire.android.emm.ManagedConfigurationsManagerImpl -import com.wire.android.util.EMPTY -import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.kalium.logic.configuration.server.ServerConfig -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Named -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class ManagedConfigurationsModule { - - @Provides - @Singleton - fun provideServerConfigProvider(): ServerConfigProvider = ServerConfigProvider() - - @Provides - @Singleton - fun provideAndroidUserContextProvider(): AndroidUserContextProvider = - AndroidUserContextProviderImpl() - - @Provides - @Singleton - fun provideManagedConfigParser( - userContextProvider: AndroidUserContextProvider - ): ManagedConfigParser = ManagedConfigParserImpl(userContextProvider) - - @Provides - @Singleton - fun provideManagedConfigurationsRepository( - @ApplicationContext context: Context, - dispatcherProvider: DispatcherProvider, - serverConfigProvider: ServerConfigProvider, - globalDataStore: GlobalDataStore, - configParser: ManagedConfigParser - ): ManagedConfigurationsManager { - return ManagedConfigurationsManagerImpl( - context, - dispatcherProvider, - serverConfigProvider, - globalDataStore, - configParser - ) - } - - @Provides - fun provideCurrentServerConfig( - managedConfigurationsManager: ManagedConfigurationsManager - ): ServerConfig.Links { - return if (BuildConfig.EMM_SUPPORT_ENABLED) { - // Returns the current resolved server configuration links, which could be either managed or default - managedConfigurationsManager.currentServerConfig - } else { - // If EMM support is disabled, always return the static default server configuration links - provideServerConfigProvider().getDefaultServerConfig(null) - } - } - - @Provides - @Named("ssoCodeConfig") - fun provideCurrentSSOCodeConfig( - managedConfigurationsManager: ManagedConfigurationsManager - ): String { - return if (BuildConfig.EMM_SUPPORT_ENABLED) { - // Returns the current resolved SSO code from managed configurations, or empty if none - managedConfigurationsManager.currentSSOCodeConfig - } else { - // If EMM support is disabled, always return empty SSO code - String.EMPTY - } - } -} diff --git a/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt b/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt index fbca5ab541e..82e7f1a1543 100644 --- a/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt +++ b/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt @@ -18,10 +18,19 @@ package com.wire.android.di import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.sebaslogen.resaca.metro.metroViewModelScoped import com.sebaslogen.resaca.KeyInScopeResolver -import com.sebaslogen.resaca.hilt.hiltViewModelScoped +import com.wire.android.di.metro.LocalMetroViewModelGraph +import com.wire.android.di.metro.WireMetroGraph +import com.wire.android.di.metro.createWireMetroGraph +import java.lang.reflect.InvocationTargetException import kotlin.time.Duration /** @@ -32,7 +41,7 @@ interface AssistedViewModelFactory { } /** - * Custom implementation of [hiltViewModelScoped] that uses our generated previews for scoped ViewModels + * Repo-local scoped ViewModel accessor that uses our generated previews for scoped ViewModels * and creates assisted injected scoped [ViewModel] instances using [ScopedArgs]. * * [ViewModel] needs to implement an interface annotated with [ViewModelScopedPreview] and with default @@ -42,21 +51,25 @@ interface AssistedViewModelFactory { * * @param arguments The arguments that will be provided to the [ViewModel]. */ -@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") +@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER", "UnusedParameter") @Composable -inline fun > - hiltViewModelScoped(arguments: R, clearDelay: Duration? = null): S where T : ViewModel, T : S = when { +inline fun + wireViewModelScoped(arguments: R, clearDelay: Duration? = null): S where T : ViewModel, T : S = when { LocalInspectionMode.current -> ViewModelScopedPreviews.firstNotNullOf { it as? S } espresso -> ViewModelScopedPreviews.firstNotNullOf { it as? S } - else -> hiltViewModelScoped(key = arguments.key?.toString(), clearDelay = clearDelay) { factory -> - factory.create(arguments) - } + else -> metroViewModelScoped( + key = arguments.key, + clearDelay = clearDelay, + factory = rememberMetroScopedViewModelFactory { + metroFactory().createWith(arguments) + }, + ) } -@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") +@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER", "UnusedParameter") @Composable -inline fun > - hiltViewModelScoped( +inline fun + wireViewModelScoped( arguments: R, noinline keyInScopeResolver: KeyInScopeResolver, clearDelay: Duration? = null, @@ -64,21 +77,22 @@ inline fun ViewModelScopedPreviews.firstNotNullOf { it as? S } espresso -> ViewModelScopedPreviews.firstNotNullOf { it as? S } else -> { - val scopedKey = requireNotNull(arguments.key?.toString()) { + requireNotNull(arguments.key?.toString()) { "Scoped key must not be null for ${T::class.qualifiedName}" } - hiltViewModelScoped( - key = scopedKey, + metroViewModelScoped( + key = arguments.key.toString(), keyInScopeResolver = keyInScopeResolver, - clearDelay = clearDelay - ) { factory -> - factory.create(arguments) - } + clearDelay = clearDelay, + factory = rememberMetroScopedViewModelFactory { + metroFactory().createWith(arguments) + }, + ) } } /** - * Custom implementation of [hiltViewModelScoped] that uses our generated previews for scoped ViewModels. + * Repo-local scoped ViewModel accessor that uses our generated previews for scoped ViewModels. * This is version that does not take and pass any arguments, it does not use any key to generate a new * version when it changes, so it basically keeps the same instance. * @@ -87,10 +101,75 @@ inline fun hiltViewModelScoped(): S where T : ViewModel, T : S = when { +inline fun wireViewModelScoped(): S where T : ViewModel, T : S = when { LocalInspectionMode.current -> ViewModelScopedPreviews.firstNotNullOf { it as? S } espresso -> ViewModelScopedPreviews.firstNotNullOf { it as? S } - else -> hiltViewModelScoped() + else -> metroViewModelScoped( + factory = rememberMetroScopedViewModelFactory { + metroFactory().createWithoutArgs() + }, + ) +} + +@PublishedApi +@Composable +internal inline fun rememberMetroScopedViewModelFactory( + crossinline create: WireMetroGraph.() -> VM, +): ViewModelProvider.Factory { + val providedGraph = LocalMetroViewModelGraph.current as? WireMetroGraph + val context = LocalContext.current + val graph = providedGraph ?: remember(context) { createWireMetroGraph(context) } + return remember(graph) { + viewModelFactory { + initializer { + graph.create() + } + } + } +} + +@PublishedApi +internal inline fun WireMetroGraph.metroFactory(): F = + this::class.java.methods + .firstOrNull { method -> + method.parameterCount == 0 && F::class.java.isAssignableFrom(method.returnType) + } + ?.invokeUnwrapped(this) + ?: error("No Metro factory matching ${F::class.qualifiedName} was provided.") + +@PublishedApi +internal inline fun Any.createWith(args: R): VM = + this::class.java.methods + .firstOrNull { method -> + method.name == "create" && + method.parameterCount == 1 && + method.parameterTypes.first().isAssignableFrom(args::class.java) + } + ?.invokeUnwrapped(this, args) + ?: error("No create(${args::class.qualifiedName}) method was provided by ${this::class.qualifiedName}.") + +@PublishedApi +internal inline fun Any.createWithoutArgs(): VM = + this::class.java.methods + .firstOrNull { method -> + method.name == "create" && + method.parameterCount == 0 && + VM::class.java.isAssignableFrom(method.returnType) + } + ?.invokeUnwrapped(this) + ?: error("No create() method returning ${VM::class.qualifiedName} was provided by ${this::class.qualifiedName}.") + +@PublishedApi +internal inline fun java.lang.reflect.Method.invokeUnwrapped(target: Any, vararg args: Any?): R = + try { + invoke(target, *args) as? R ?: error("${declaringClass.name}#$name returned a value that is not ${R::class.qualifiedName}.") + } catch (exception: InvocationTargetException) { + throw exception.targetException ?: exception + } + +@PublishedApi +internal data object EmptyScopedArgs : ScopedArgs { + override val key: Any? = null } val espresso diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/AppsModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/AppsModule.kt index 6eb57cd996a..d438cf8c4e2 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/AppsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/AppsModule.kt @@ -16,50 +16,3 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.wire.android.di.accountScoped - -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.feature.app.AppScope -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.app.GetAppByIdUseCase -import com.wire.kalium.logic.feature.app.ObserveAllAppsUseCase -import com.wire.kalium.logic.feature.app.ObserveIsAppMemberUseCase -import com.wire.kalium.logic.feature.app.SearchAppsByNameUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -class AppsModule { - - @ViewModelScoped - @Provides - fun provideAppScope( - @CurrentAccount currentAccount: UserId, - @KaliumCoreLogic coreLogic: CoreLogic - ): AppScope = coreLogic.getSessionScope(currentAccount).apps - - @ViewModelScoped - @Provides - fun provideGetAppByIdUseCase(appScope: AppScope): GetAppByIdUseCase = - appScope.getAppById - - @ViewModelScoped - @Provides - fun provideObserveIsAppMemberUseCase(appScope: AppScope): ObserveIsAppMemberUseCase = - appScope.observeIsAppMember - - @ViewModelScoped - @Provides - fun provideSearchAppsByNameUseCase(appScope: AppScope): SearchAppsByNameUseCase = - appScope.searchAppsByName - - @ViewModelScoped - @Provides - fun provideObserveAllAppsUseCase(appScope: AppScope): ObserveAllAppsUseCase = - appScope.observeAllApps -} diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/AuthenticationModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/AuthenticationModule.kt index c60b6b2b105..e3e2b8dcf5b 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/AuthenticationModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/AuthenticationModule.kt @@ -17,31 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.auth.AuthenticationScope -import com.wire.kalium.logic.feature.auth.verification.RequestSecondFactorVerificationCodeUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -class AuthenticationModule { - - @Provides - @ViewModelScoped - fun provideAuthenticationScope( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): AuthenticationScope = coreLogic.getSessionScope(currentAccount).authenticationScope - - @ViewModelScoped - @Provides - fun provideRequest2FACodeUseCase(authenticationScope: AuthenticationScope): RequestSecondFactorVerificationCodeUseCase = - authenticationScope.requestSecondFactorVerificationCode -} +// Account-scoped authentication bindings are provided by the Metro graph. diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/BackupModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/BackupModule.kt index 134fcdc1e44..e23b34a33d8 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/BackupModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/BackupModule.kt @@ -17,64 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.BuildConfig -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.android.ui.home.settings.backup.MPBackupSettings -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.backup.BackupScope -import com.wire.kalium.util.DelicateKaliumApi -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -class BackupModule { - - @ViewModelScoped - @Provides - fun provideBackupScope(@KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId): BackupScope = - coreLogic.getSessionScope(currentAccount).backup - - @ViewModelScoped - @Provides - fun provideCreateBackupUseCase(backupScope: BackupScope) = - backupScope.create - - @ViewModelScoped - @Provides - fun provideVerifyBackupUseCase(backupScope: BackupScope) = - backupScope.verify - - @ViewModelScoped - @Provides - fun provideRestoreBackupUseCase(backupScope: BackupScope) = - backupScope.restore - - @Provides - fun provideMpBackupSettings() = if (BuildConfig.ENABLE_CROSSPLATFORM_BACKUP) { - MPBackupSettings.Enabled - } else { - MPBackupSettings.Disabled - } - - @OptIn(DelicateKaliumApi::class) - @ViewModelScoped - @Provides - fun provideOnboardingBackupUseCase(backupScope: BackupScope) = - backupScope.createUnEncryptedCopy - - @ViewModelScoped - @Provides - fun provideBackupAndUploadCryptoState(backupScope: BackupScope) = - backupScope.backupAndUploadCryptoState - - @ViewModelScoped - @Provides - fun provideSetLastDeviceIdUseCase(backupScope: BackupScope) = - backupScope.setLastDeviceId -} +// Backup account-scoped providers are owned by WireMetroGraph or retrieved directly from Kalium scopes. 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..c1f9aba3272 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 @@ -17,215 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.call.CallsScope -import com.wire.kalium.logic.feature.call.usecase.EndCallOnConversationChangeUseCase -import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase -import com.wire.kalium.logic.feature.call.usecase.FlipToBackCameraUseCase -import com.wire.kalium.logic.feature.call.usecase.FlipToFrontCameraUseCase -import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase -import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase -import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase -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.SetUIRotationUseCase -import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase -import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase -import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase -import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase -import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase -import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase -import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -@Suppress("TooManyFunctions") -class CallsModule { - - @ViewModelScoped - @Provides - fun providesCallsScope( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): CallsScope = coreLogic.getSessionScope(currentAccount).calls - - @ViewModelScoped - @Provides - fun provideGetIncomingCallsUseCase(callsScope: CallsScope): GetIncomingCallsUseCase = - callsScope.getIncomingCalls - - @ViewModelScoped - @Provides - fun provideRequestVideoStreamsUseCase(callsScope: CallsScope) = - callsScope.requestVideoStreams - - @ViewModelScoped - @Provides - fun provideIsLastCallClosedUseCase(callsScope: CallsScope) = - callsScope.isLastCallClosed - - @ViewModelScoped - @Provides - fun provideObserveOngoingCallsUseCase(callsScope: CallsScope) = - callsScope.observeOngoingCalls - - @ViewModelScoped - @Provides - fun provideObserveEstablishedCallWithSortedParticipantsUseCase(callsScope: CallsScope) = - callsScope.observeEstablishedCallWithSortedParticipants - - @ViewModelScoped - @Provides - fun provideObserveLastActiveCallWithSortedParticipantsUseCase(callsScope: CallsScope) = - callsScope.observeLastActiveCallWithSortedParticipants - - @ViewModelScoped - @Provides - fun provideRejectCallUseCase(callsScope: CallsScope) = - callsScope.rejectCall - - @ViewModelScoped - @Provides - fun provideAcceptCallUseCase(callsScope: CallsScope) = - callsScope.answerCall - - @ViewModelScoped - @Provides - fun provideOnGoingCallUseCase( - callsScope: CallsScope - ): ObserveEstablishedCallsUseCase = - callsScope.establishedCall - - @ViewModelScoped - @Provides - fun provideObserveOutgoingCallUseCase( - callsScope: CallsScope - ): ObserveOutgoingCallUseCase = - callsScope.observeOutgoingCall - - @ViewModelScoped - @Provides - fun provideStartCallUseCase(callsScope: CallsScope): StartCallUseCase = - callsScope.startCall - - @ViewModelScoped - @Provides - fun provideEndCallUseCase(callsScope: CallsScope): EndCallUseCase = - callsScope.endCall - - @ViewModelScoped - @Provides - fun provideEndCallOnConversationChangeUseCase( - callsScope: CallsScope - ): EndCallOnConversationChangeUseCase = - callsScope.endCallOnConversationChange - - @ViewModelScoped - @Provides - fun provideMuteCallUseCase(callsScope: CallsScope): MuteCallUseCase = - callsScope.muteCall - - @ViewModelScoped - @Provides - fun provideUnMuteCallUseCase(callsScope: CallsScope): UnMuteCallUseCase = - callsScope.unMuteCall - - @ViewModelScoped - @Provides - fun provideSetVideoPreviewUseCase( - callsScope: CallsScope - ): SetVideoPreviewUseCase = callsScope.setVideoPreview - - @ViewModelScoped - @Provides - fun provideSetUIRotationUseCase( - callsScope: CallsScope - ): SetUIRotationUseCase = callsScope.setUIRotation - - @ViewModelScoped - @Provides - fun provideFlipToBackCameraUseCase( - callsScope: CallsScope - ): FlipToBackCameraUseCase = callsScope.flipToBackCamera - - @ViewModelScoped - @Provides - fun provideFlipToFrontCameraUseCase( - callsScope: CallsScope - ): FlipToFrontCameraUseCase = callsScope.flipToFrontCamera - - @ViewModelScoped - @Provides - fun turnLoudSpeakerOffUseCaseProvider( - callsScope: CallsScope - ): TurnLoudSpeakerOffUseCase = callsScope.turnLoudSpeakerOff - - @ViewModelScoped - @Provides - fun provideTurnLoudSpeakerOnUseCase( - callsScope: CallsScope - ): TurnLoudSpeakerOnUseCase = callsScope.turnLoudSpeakerOn - - @ViewModelScoped - @Provides - fun provideObserveSpeakerUseCase( - callsScope: CallsScope - ): ObserveSpeakerUseCase = callsScope.observeSpeaker - - @ViewModelScoped - @Provides - fun provideUpdateVideoStateUseCase( - callsScope: CallsScope - ): UpdateVideoStateUseCase = - callsScope.updateVideoState - - @ViewModelScoped - @Provides - fun provideSetVideoSendStateUseCase( - callsScope: CallsScope - ): SetVideoSendStateUseCase = - callsScope.setVideoSendState - - @ViewModelScoped - @Provides - fun provideIsCallRunningUseCase(callsScope: CallsScope) = - callsScope.isCallRunning - - @ViewModelScoped - @Provides - fun provideIsEligibleToStartCall(callsScope: CallsScope) = - callsScope.isEligibleToStartCall - - @ViewModelScoped - @Provides - fun provideObserveConferenceCallingEnabledUseCase(callsScope: CallsScope) = - callsScope.observeConferenceCallingEnabled - - @ViewModelScoped - @Provides - fun provideObserveInCallReactionsUseCase(callsScope: CallsScope) = - callsScope.observeInCallReactions - - @ViewModelScoped - @Provides - fun provideObserveCallQualityDataUseCase(callsScope: CallsScope) = - callsScope.observeCallQualityData - - @ViewModelScoped - @Provides - fun provideSetCallQualityIntervalUseCase(callsScope: CallsScope) = - callsScope.setCallQualityInterval - - @ViewModelScoped - @Provides - fun provideObserveCallModerationActionsUseCase(callsScope: CallsScope) = - callsScope.observeCallModerationActions -} +// Calls account-scoped providers are owned by WireMetroGraph. diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt index 4c8e5772fcc..51733838806 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt @@ -17,246 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.android.feature.cells.util.FileNameResolver -import com.wire.android.ui.home.conversations.model.messagetypes.multipart.CellAssetRefreshHelper -import com.wire.kalium.cells.CellsScope -import com.wire.kalium.cells.domain.CellUploadManager -import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCase -import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase -import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase -import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase -import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase -import com.wire.kalium.cells.domain.usecase.GetFoldersUseCase -import com.wire.kalium.cells.domain.usecase.GetMessageAttachmentUseCase -import com.wire.kalium.cells.domain.usecase.GetOwnersUseCase -import com.wire.kalium.cells.domain.usecase.GetPaginatedCellConversationsFlowUseCase -import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase -import com.wire.kalium.cells.domain.usecase.GetPaginatedNodesUseCase -import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase -import com.wire.kalium.cells.domain.usecase.IsAtLeastOneCellAvailableUseCase -import com.wire.kalium.cells.domain.usecase.MoveNodeUseCase -import com.wire.kalium.cells.domain.usecase.ObserveAttachmentDraftsUseCase -import com.wire.kalium.cells.domain.usecase.PublishAttachmentsUseCase -import com.wire.kalium.cells.domain.usecase.RefreshCellAssetStateUseCase -import com.wire.kalium.cells.domain.usecase.RemoveAttachmentDraftUseCase -import com.wire.kalium.cells.domain.usecase.RemoveAttachmentDraftsUseCase -import com.wire.kalium.cells.domain.usecase.RemoveNodeTagsUseCase -import com.wire.kalium.cells.domain.usecase.RenameNodeUseCase -import com.wire.kalium.cells.domain.usecase.RestoreNodeFromRecycleBinUseCase -import com.wire.kalium.cells.domain.usecase.RetryAttachmentUploadUseCase -import com.wire.kalium.cells.domain.usecase.UpdateNodeTagsUseCase -import com.wire.kalium.cells.domain.usecase.create.CreateDocumentFileUseCase -import com.wire.kalium.cells.domain.usecase.create.CreateFolderUseCase -import com.wire.kalium.cells.domain.usecase.create.CreatePresentationFileUseCase -import com.wire.kalium.cells.domain.usecase.create.CreateSpreadsheetFileUseCase -import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase -import com.wire.kalium.cells.domain.usecase.download.DownloadCellVersionUseCase -import com.wire.kalium.cells.domain.usecase.publiclink.CreatePublicLinkPasswordUseCase -import com.wire.kalium.cells.domain.usecase.publiclink.CreatePublicLinkUseCase -import com.wire.kalium.cells.domain.usecase.publiclink.DeletePublicLinkUseCase -import com.wire.kalium.cells.domain.usecase.publiclink.GetPublicLinkPasswordUseCase -import com.wire.kalium.cells.domain.usecase.publiclink.GetPublicLinkUseCase -import com.wire.kalium.cells.domain.usecase.publiclink.SetPublicLinkExpirationUseCase -import com.wire.kalium.cells.domain.usecase.publiclink.UpdatePublicLinkPasswordUseCase -import com.wire.kalium.cells.domain.usecase.versioning.GetNodeVersionsUseCase -import com.wire.kalium.cells.domain.usecase.versioning.RestoreNodeVersionUseCase -import com.wire.kalium.cells.paginatedConversationsFlowUseCase -import com.wire.kalium.cells.paginatedFilesFlowUseCase -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.featureFlags.KaliumConfigs -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Suppress("TooManyFunctions") -@Module -@InstallIn(ViewModelComponent::class) -class CellsModule { - - @ViewModelScoped - @Provides - fun provideCellsScope( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount accountId: UserId, - ): CellsScope = coreLogic.getSessionScope(accountId).cells - - @ViewModelScoped - @Provides - fun provideAddAttachmentUseCase(cellsScope: CellsScope): AddAttachmentDraftUseCase = cellsScope.addAttachment - - @ViewModelScoped - @Provides - fun provideRemoveAttachmentUseCase(cellsScope: CellsScope): RemoveAttachmentDraftUseCase = cellsScope.removeAttachment - - @ViewModelScoped - @Provides - fun provideRemoveAttachmentsUseCase(cellsScope: CellsScope): RemoveAttachmentDraftsUseCase = cellsScope.removeAttachments - - @ViewModelScoped - @Provides - fun provideObserveAttachmentsUseCase(cellsScope: CellsScope): ObserveAttachmentDraftsUseCase = cellsScope.observeAttachments - - @ViewModelScoped - @Provides - fun providePublishAttachmentsUseCase(cellsScope: CellsScope): PublishAttachmentsUseCase = cellsScope.publishAttachments - - @ViewModelScoped - @Provides - fun provideCellUploadManager(cellsScope: CellsScope): CellUploadManager = cellsScope.uploadManager - - @ViewModelScoped - @Provides - fun provideObserveFilesUseCase(cellsScope: CellsScope): GetPaginatedNodesUseCase = cellsScope.observeFiles - - @ViewModelScoped - @Provides - fun provideObservePagedFilesUseCase(cellsScope: CellsScope): GetPaginatedFilesFlowUseCase = cellsScope.paginatedFilesFlowUseCase - - @ViewModelScoped - @Provides - fun provideDownloadUseCase(cellsScope: CellsScope): DownloadCellFileUseCase = cellsScope.downloadCellFile - - @ViewModelScoped - @Provides - fun provideRefreshAssetUseCase(cellsScope: CellsScope): RefreshCellAssetStateUseCase = cellsScope.refreshAsset - - @ViewModelScoped - @Provides - fun provideDeleteCellAssetUseCase(cellsScope: CellsScope): DeleteCellAssetUseCase = cellsScope.deleteCellAssetUseCase - - @ViewModelScoped - @Provides - fun provideCreatePublicUrlUseCase(cellsScope: CellsScope): CreatePublicLinkUseCase = cellsScope.createPublicLinkUseCase - - @ViewModelScoped - @Provides - fun provideGetPublicUrlUseCase(cellsScope: CellsScope): GetPublicLinkUseCase = cellsScope.getPublicLinkUseCase - - @ViewModelScoped - @Provides - fun provideDeletePublicUrlUseCase(cellsScope: CellsScope): DeletePublicLinkUseCase = cellsScope.deletePublicLinkUseCase - - @ViewModelScoped - @Provides - fun provideRetryAttachmentUploadUseCase(cellsScope: CellsScope): RetryAttachmentUploadUseCase = cellsScope.retryAttachmentUpload - - @ViewModelScoped - @Provides - fun provideCreateFolderUseCase(cellsScope: CellsScope): CreateFolderUseCase = cellsScope.createFolderUseCase - - @ViewModelScoped - @Provides - fun provideCreateSpreadsheetFileUseCase(cellsScope: CellsScope): CreateSpreadsheetFileUseCase = cellsScope.createSpreadsheetFileUseCase - - @ViewModelScoped - @Provides - fun provideCreateDocumentFileUseCase(cellsScope: CellsScope): CreateDocumentFileUseCase = cellsScope.createDocumentFileUseCase - - @ViewModelScoped - @Provides - fun provideCreatePresentationFileUseCase(cellsScope: CellsScope): CreatePresentationFileUseCase = - cellsScope.createPresentationFileUseCase - - @ViewModelScoped - @Provides - fun provideMoveNodeUseCase(cellsScope: CellsScope): MoveNodeUseCase = cellsScope.moveNodeUseCase - - @ViewModelScoped - @Provides - fun provideGetFoldersUseCase(cellsScope: CellsScope): GetFoldersUseCase = cellsScope.getFoldersUseCase - - @ViewModelScoped - @Provides - fun provideRestoreNodeFromRecycleBinUseCase(cellsScope: CellsScope): RestoreNodeFromRecycleBinUseCase = - cellsScope.restoreNodeFromRecycleBin - - @ViewModelScoped - @Provides - fun provideRenameNodeUseCase(cellsScope: CellsScope): RenameNodeUseCase = - cellsScope.renameNodeUseCase - - @ViewModelScoped - @Provides - fun provideGetAllTagsUseCase(cellsScope: CellsScope): GetAllTagsUseCase = cellsScope.getAllTags - - @ViewModelScoped - @Provides - fun provideUpdateNodeTagsUseCase(cellsScope: CellsScope): UpdateNodeTagsUseCase = cellsScope.updateNodeTagsUseCase - - @ViewModelScoped - @Provides - fun provideRemoveNodeTagsUseCase(cellsScope: CellsScope): RemoveNodeTagsUseCase = cellsScope.removeNodeTagsUseCase - - @ViewModelScoped - @Provides - fun provideCellAvailableUseCase(cellsScope: CellsScope): IsAtLeastOneCellAvailableUseCase = cellsScope.isCellAvailable - - @ViewModelScoped - @Provides - fun provideGetAttachmentUseCase(cellsScope: CellsScope): GetMessageAttachmentUseCase = cellsScope.getMessageAttachmentUseCase - - @ViewModelScoped - @Provides - fun provideGetOwnersUseCase(cellsScope: CellsScope): GetOwnersUseCase = cellsScope.getOwnersUseCase - - @Provides - fun provideFileNameResolver(): FileNameResolver = FileNameResolver() - - @Provides - fun provideGetCellNodeUseCase(cellsScope: CellsScope): GetCellFileUseCase = cellsScope.getCellFileUseCase - - @Provides - fun provideCreatePublicLinkPasswordUseCase(cellsScope: CellsScope): CreatePublicLinkPasswordUseCase = - cellsScope.createPublicLinkPasswordUseCase - - @Provides - fun provideUpdatePublicLinkPasswordUseCase(cellsScope: CellsScope): UpdatePublicLinkPasswordUseCase = - cellsScope.updatePublicLinkPasswordUseCase - - @Provides - fun provideGetPublicLinkPasswordUseCase(cellsScope: CellsScope): GetPublicLinkPasswordUseCase = - cellsScope.getPublicLinkPassword - - @Provides - fun provideSetPublicLinkExpirationUseCase(cellsScope: CellsScope): SetPublicLinkExpirationUseCase = - cellsScope.setPublicLinkExpiration - - @Provides - fun provideEditorUrlUseCase(cellsScope: CellsScope): GetEditorUrlUseCase = cellsScope.getEditorUrl - - @ViewModelScoped - @Provides - fun provideGetNodeVersionsUseCase(cellsScope: CellsScope): GetNodeVersionsUseCase = - cellsScope.getNodeVersions - - @ViewModelScoped - @Provides - fun provideRestoreNodeVersionUseCase(cellsScope: CellsScope): RestoreNodeVersionUseCase = - cellsScope.restoreNodeVersion - - @ViewModelScoped - @Provides - fun provideDownloadCellVersionUseCase(cellsScope: CellsScope): DownloadCellVersionUseCase = - cellsScope.downloadCellVersion - - @ViewModelScoped - @Provides - fun provideRefreshHelper(cellsScope: CellsScope, kaliumConfigs: KaliumConfigs): CellAssetRefreshHelper = CellAssetRefreshHelper( - refreshAsset = cellsScope.refreshAsset, - featureFlags = kaliumConfigs - ) - - @ViewModelScoped - @Provides - fun provideGetCellsConfigUseCase(cellsScope: CellsScope): GetWireCellConfigurationUseCase = cellsScope.getCellConfig - - @ViewModelScoped - @Provides - fun provideGetPaginatedConversationsFlowUseCase(cellsScope: CellsScope): GetPaginatedCellConversationsFlowUseCase = - cellsScope.paginatedConversationsFlowUseCase -} +// Cells account-scoped providers are owned by WireMetroGraph. diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/ChannelsModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/ChannelsModule.kt index 57829c186d6..c3e15b5733e 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ChannelsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/ChannelsModule.kt @@ -17,37 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.channels.ChannelsScope -import com.wire.kalium.logic.feature.channels.ObserveChannelsCreationPermissionUseCase -import com.wire.kalium.logic.feature.conversation.channel.UpdateChannelAddPermissionUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -class ChannelsModule { - - @Provides - @ViewModelScoped - fun provideChannelsScope( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): ChannelsScope = coreLogic.getSessionScope(currentAccount).channels - - @ViewModelScoped - @Provides - fun provideUpdateChannelAddPermission(channelsScope: ChannelsScope): UpdateChannelAddPermissionUseCase = - channelsScope.updateChannelAddPermission - - @ViewModelScoped - @Provides - fun provideChannelCreationPermissionUseCase(channelsScope: ChannelsScope): ObserveChannelsCreationPermissionUseCase = - channelsScope.observeChannelsCreationPermissionUseCase -} +// Channels account-scoped providers are owned by WireMetroGraph. diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/ClientModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/ClientModule.kt index 6d13001ac90..e335fb38a79 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ClientModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/ClientModule.kt @@ -17,95 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.client.ClientFingerprintUseCase -import com.wire.kalium.logic.feature.client.ClientScope -import com.wire.kalium.logic.feature.client.DeleteClientUseCase -import com.wire.kalium.logic.feature.client.FetchSelfClientsFromRemoteUseCase -import com.wire.kalium.logic.feature.client.FetchUsersClientsFromRemoteUseCase -import com.wire.kalium.logic.feature.client.GetOrRegisterClientUseCase -import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase -import com.wire.kalium.logic.feature.client.ObserveClientDetailsUseCase -import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase -import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase -import com.wire.kalium.logic.feature.client.UpdateClientVerificationStatusUseCase -import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase -import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -class ClientModule { - - @ViewModelScoped - @Provides - fun provideClientScopeProvider( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): ClientScope = coreLogic.getSessionScope(currentAccount).client - - @ViewModelScoped - @Provides - fun provideMlsKeyPackageCountUseCase(clientScope: ClientScope): MLSKeyPackageCountUseCase = - clientScope.mlsKeyPackageCountUseCase - - @ViewModelScoped - @Provides - fun provideRestartSlowSyncProcessForRecoveryUseCase(clientScope: ClientScope): RestartSlowSyncProcessForRecoveryUseCase = - clientScope.restartSlowSyncProcessForRecoveryUseCase - - @ViewModelScoped - @Provides - fun provideDeleteClientUseCase(clientScope: ClientScope): DeleteClientUseCase = clientScope.deleteClient - - @ViewModelScoped - @Provides - fun provideGetOrRegisterClientUseCase(clientScope: ClientScope): GetOrRegisterClientUseCase = - clientScope.getOrRegister - - @ViewModelScoped - @Provides - fun provideFetchUsersClientsFromRemoteUseCase(clientScope: ClientScope): FetchUsersClientsFromRemoteUseCase = - clientScope.fetchUsersClients - - @ViewModelScoped - @Provides - fun provideGetOtherUsersClients(clientScope: ClientScope): ObserveClientsByUserIdUseCase = - clientScope.getOtherUserClients - - @ViewModelScoped - @Provides - fun provideFetchSelfClientsFromRemoteUseCase(clientScope: ClientScope): FetchSelfClientsFromRemoteUseCase = - clientScope.fetchSelfClients - - @ViewModelScoped - @Provides - fun provideClientFingerPrintUseCase(clientScope: ClientScope): ClientFingerprintUseCase = - clientScope.remoteClientFingerPrint - - @ViewModelScoped - @Provides - fun provideUpdateClientVerificationStatusUseCase(clientScope: ClientScope): UpdateClientVerificationStatusUseCase = - clientScope.updateClientVerificationStatus - - @ViewModelScoped - @Provides - fun provideGetClientDetailsUseCase(clientScope: ClientScope): ObserveClientDetailsUseCase = clientScope.observeClientDetailsUseCase - - @ViewModelScoped - @Provides - fun provideObserveCurrentClientUseCase(clientScope: ClientScope): ObserveCurrentClientIdUseCase = - clientScope.observeCurrentClientId - - @ViewModelScoped - @Provides - fun provideNeedsToRegisterClientUseCase(clientScope: ClientScope): NeedsToRegisterClientUseCase = - clientScope.needsToRegisterClient -} +// Account-scoped client bindings are provided by the Metro graph. diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConnectionModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConnectionModule.kt index 513838404fc..f572da610f4 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConnectionModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConnectionModule.kt @@ -17,45 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.connection.ConnectionScope -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -class ConnectionModule { - - @ViewModelScoped - @Provides - fun provideConnectionScope( - @CurrentAccount currentAccount: UserId, - @KaliumCoreLogic coreLogic: CoreLogic - ): ConnectionScope = coreLogic.getSessionScope(currentAccount).connection - - @ViewModelScoped - @Provides - fun provideSendConnectionRequestUseCase(connectionScope: ConnectionScope) = - connectionScope.sendConnectionRequest - - @ViewModelScoped - @Provides - fun provideCancelConnectionRequestUseCase(connectionScope: ConnectionScope) = - connectionScope.cancelConnectionRequest - - @ViewModelScoped - @Provides - fun provideIgnoreConnectionRequestUseCase(connectionScope: ConnectionScope) = - connectionScope.ignoreConnectionRequest - - @ViewModelScoped - @Provides - fun provideAcceptConnectionRequestUseCase(connectionScope: ConnectionScope) = - connectionScope.acceptConnectionRequest -} +// Connection account-scoped providers are owned by WireMetroGraph. diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt index f0379b34b56..8206a2e49d1 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt @@ -17,406 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.conversation.FetchConversationUseCase -import com.wire.kalium.logic.data.conversation.ResetMLSConversationUseCase -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.conversation.AddMemberToConversationUseCase -import com.wire.kalium.logic.feature.conversation.AddServiceToConversationUseCase -import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase -import com.wire.kalium.logic.feature.conversation.ClearUsersTypingEventsUseCase -import com.wire.kalium.logic.feature.conversation.ConversationScope -import com.wire.kalium.logic.feature.conversation.GetConversationProtocolInfoUseCase -import com.wire.kalium.logic.feature.conversation.GetConversationUnreadEventsCountUseCase -import com.wire.kalium.logic.feature.conversation.GetOneToOneConversationDetailsUseCase -import com.wire.kalium.logic.feature.conversation.GetOrCreateOneToOneConversationUseCase -import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedUseCase -import com.wire.kalium.logic.feature.conversation.JoinConversationViaCodeUseCase -import com.wire.kalium.logic.feature.conversation.CheckConversationLeaveConditionsUseCase -import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase -import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConversationAdminRoleUseCase -import com.wire.kalium.logic.feature.conversation.PromoteAdminAndLeaveConversationUseCase -import com.wire.kalium.logic.feature.conversation.NotifyConversationIsOpenUseCase -import com.wire.kalium.logic.feature.conversation.ObserveArchivedUnreadConversationsCountUseCase -import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase -import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase -import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsUseCase -import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase -import com.wire.kalium.logic.feature.conversation.ObserveConversationUnderLegalHoldNotifiedUseCase -import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase -import com.wire.kalium.logic.feature.conversation.ObserveIsSelfUserMemberUseCase -import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase -import com.wire.kalium.logic.feature.conversation.ObserveUsersTypingUseCase -import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase -import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase -import com.wire.kalium.logic.feature.conversation.RenameConversationUseCase -import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase -import com.wire.kalium.logic.feature.conversation.SetNotifiedAboutConversationUnderLegalHoldUseCase -import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase -import com.wire.kalium.logic.feature.conversation.SyncConversationCodeUseCase -import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUseCase -import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase -import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUseCase -import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase -import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase -import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase -import com.wire.kalium.logic.feature.conversation.UpdateConversationReceiptModeUseCase -import com.wire.kalium.logic.feature.conversation.apps.ChangeAccessForAppsInConversationUseCase -import com.wire.kalium.logic.feature.conversation.createconversation.CreateChannelUseCase -import com.wire.kalium.logic.feature.conversation.createconversation.CreateRegularGroupUseCase -import com.wire.kalium.logic.feature.conversation.delete.MarkConversationAsDeletedLocallyUseCase -import com.wire.kalium.logic.feature.conversation.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery -import com.wire.kalium.logic.feature.conversation.guestroomlink.CanCreatePasswordProtectedLinksUseCase -import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase -import com.wire.kalium.logic.feature.conversation.guestroomlink.ObserveGuestRoomLinkUseCase -import com.wire.kalium.logic.feature.conversation.guestroomlink.RevokeGuestRoomLinkUseCase -import com.wire.kalium.logic.feature.conversation.messagetimer.UpdateMessageTimerUseCase -import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -@Suppress("TooManyFunctions") -class ConversationModule { - - @Provides - @ViewModelScoped - fun provideConversationScope( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): ConversationScope = coreLogic.getSessionScope(currentAccount).conversations - - @ViewModelScoped - @Provides - fun provideObserveConversationListDetails(conversationScope: ConversationScope): ObserveConversationListDetailsUseCase = - conversationScope.observeConversationListDetails - - @ViewModelScoped - @Provides - fun provideObserveConversationListDetailsWithEvents(conversationScope: ConversationScope) = - conversationScope.observeConversationListDetailsWithEvents - - @ViewModelScoped - @Provides - fun provideObserveConversationUseCase(conversationScope: ConversationScope): GetOneToOneConversationDetailsUseCase = - conversationScope.getOneToOneConversation - - @ViewModelScoped - @Provides - fun provideObserveConversationDetailsUseCase(conversationScope: ConversationScope): ObserveConversationDetailsUseCase = - conversationScope.observeConversationDetails - - @ViewModelScoped - @Provides - fun provideNotifyConversationIsOpenUseCase(conversationScope: ConversationScope): NotifyConversationIsOpenUseCase = - conversationScope.notifyConversationIsOpen - - @ViewModelScoped - @Provides - fun provideDeleteTeamConversationUseCase(conversationScope: ConversationScope): DeleteTeamConversationUseCase = - conversationScope.deleteTeamConversation - - @ViewModelScoped - @Provides - fun provideMarkConversationAsDeletedLocallyUseCase(conversationScope: ConversationScope): MarkConversationAsDeletedLocallyUseCase = - conversationScope.markConversationAsDeletedLocallyUseCase - - @ViewModelScoped - @Provides - fun provideObserveIsSelfConversationMemberUseCase(conversationScope: ConversationScope): ObserveIsSelfUserMemberUseCase = - conversationScope.observeIsSelfUserMemberUseCase - - @ViewModelScoped - @Provides - fun provideObserveConversationInteractionAvailability( - conversationScope: ConversationScope - ): ObserveConversationInteractionAvailabilityUseCase = - conversationScope.observeConversationInteractionAvailabilityUseCase - - @ViewModelScoped - @Provides - fun provideObserveConversationMembersUseCase(conversationScope: ConversationScope): ObserveConversationMembersUseCase = - conversationScope.observeConversationMembers - - @ViewModelScoped - @Provides - fun provideMembersToMentionUseCase(conversationScope: ConversationScope) = - conversationScope.getMembersToMention - - @ViewModelScoped - @Provides - fun provideObserveUserListByIdUseCase(conversationScope: ConversationScope): ObserveUserListByIdUseCase = - conversationScope.observeUserListById - - @ViewModelScoped - @Provides - fun providesJoinConversationViaCodeUseCase(conversationScope: ConversationScope): JoinConversationViaCodeUseCase = - conversationScope.joinConversationViaCode - - @ViewModelScoped - @Provides - fun providesCanCreatePasswordProtectedLinksUseCase(conversationScope: ConversationScope): CanCreatePasswordProtectedLinksUseCase = - conversationScope.canCreatePasswordProtectedLinks - - @ViewModelScoped - @Provides - fun provideRefreshConversationsWithoutMetadataUseCase( - conversationScope: ConversationScope - ): RefreshConversationsWithoutMetadataUseCase = - conversationScope.refreshConversationsWithoutMetadata - - @ViewModelScoped - @Provides - fun provideGetConversationUnreadEventsCountUseCase(conversationScope: ConversationScope): GetConversationUnreadEventsCountUseCase = - conversationScope.getConversationUnreadEventsCountUseCase - - @ViewModelScoped - @Provides - fun provideUpdateMessageTimerUseCase(conversationScope: ConversationScope): UpdateMessageTimerUseCase = - conversationScope.updateMessageTimer - - @ViewModelScoped - @Provides - fun provideRenameConversation(conversationScope: ConversationScope): RenameConversationUseCase = - conversationScope.renameConversation - - @ViewModelScoped - @Provides - fun provideUpdateConversationReadDateUseCase(conversationScope: ConversationScope): UpdateConversationReadDateUseCase = - conversationScope.updateConversationReadDateUseCase - - @ViewModelScoped - @Provides - fun provideMarkConversationAsReadLocallyUseCase(conversationScope: ConversationScope): MarkConversationAsReadLocallyUseCase = - conversationScope.markConversationAsReadLocally - - @ViewModelScoped - @Provides - fun provideUpdateConversationAccessUseCase(conversationScope: ConversationScope): UpdateConversationAccessRoleUseCase = - conversationScope.updateConversationAccess - - @ViewModelScoped - @Provides - fun provideLeaveConversationUseCase(conversationScope: ConversationScope): LeaveConversationUseCase = - conversationScope.leaveConversation - - @ViewModelScoped - @Provides - fun providePromoteAdminAndLeaveConversationUseCase(conversationScope: ConversationScope): PromoteAdminAndLeaveConversationUseCase = - conversationScope.promoteAdminAndLeaveConversation - - @ViewModelScoped - @Provides - fun provideCheckConversationLeaveConditionsUseCase(conversationScope: ConversationScope): CheckConversationLeaveConditionsUseCase = - conversationScope.checkConversationLeaveConditions - - @ViewModelScoped - @Provides - fun provideObserveEligibleMembersForConversationAdminRoleUseCase( - conversationScope: ConversationScope - ): ObserveEligibleMembersForConversationAdminRoleUseCase = - conversationScope.observeEligibleMembersForConversationAdminRole - - @ViewModelScoped - @Provides - fun provideUpdateConversationMutedStatusUseCase(conversationScope: ConversationScope): UpdateConversationMutedStatusUseCase = - conversationScope.updateConversationMutedStatus - - @ViewModelScoped - @Provides - fun provideUpdateConversationReceiptModeUseCase(conversationScope: ConversationScope): UpdateConversationReceiptModeUseCase = - conversationScope.updateConversationReceiptMode - - @ViewModelScoped - @Provides - fun provideAddServiceToConversationUseCase(conversationScope: ConversationScope): AddServiceToConversationUseCase = - conversationScope.addServiceToConversationUseCase - - @ViewModelScoped - @Provides - fun provideRemoveMemberFromConversationUseCase(conversationScope: ConversationScope): RemoveMemberFromConversationUseCase = - conversationScope.removeMemberFromConversation - - @ViewModelScoped - @Provides - fun provideCreateRegularGroupUseCase(conversationScope: ConversationScope): CreateRegularGroupUseCase = - conversationScope.createRegularGroup - - @ViewModelScoped - @Provides - fun provideCreateChannelUseCase(conversationScope: ConversationScope): CreateChannelUseCase = - conversationScope.createChannel - - @ViewModelScoped - @Provides - fun provideAddMemberToConversationUseCase(conversationScope: ConversationScope): AddMemberToConversationUseCase = - conversationScope.addMemberToConversationUseCase - - @ViewModelScoped - @Provides - fun provideGetOrCreateOneToOneConversationUseCase(conversationScope: ConversationScope): GetOrCreateOneToOneConversationUseCase = - conversationScope.getOrCreateOneToOneConversationUseCase - - @ViewModelScoped - @Provides - fun provideGenerateGuestRoomLinkUseCase(conversationScope: ConversationScope): GenerateGuestRoomLinkUseCase = - conversationScope.generateGuestRoomLink - - @ViewModelScoped - @Provides - fun provideRevokeGuestRoomLinkUseCase(conversationScope: ConversationScope): RevokeGuestRoomLinkUseCase = - conversationScope.revokeGuestRoomLink - - @ViewModelScoped - @Provides - fun provideObserveGuestRoomLinkUseCase(conversationScope: ConversationScope): ObserveGuestRoomLinkUseCase = - conversationScope.observeGuestRoomLink - - @ViewModelScoped - @Provides - fun provideClearConversationContentUseCase(conversationScope: ConversationScope): ClearConversationContentUseCase = - conversationScope.clearConversationContent - - @ViewModelScoped - @Provides - fun provideUpdateConversationArchivedStatusUseCase(conversationScope: ConversationScope): UpdateConversationArchivedStatusUseCase = - conversationScope.updateConversationArchivedStatus - - @ViewModelScoped - @Provides - fun provideUpdateConversationMemberRoleUseCase(conversationScope: ConversationScope): UpdateConversationMemberRoleUseCase = - conversationScope.updateConversationMemberRole - - @ViewModelScoped - @Provides - fun provideObserveArchivedUnreadConversationsCountUseCase( - conversationScope: ConversationScope - ): ObserveArchivedUnreadConversationsCountUseCase = conversationScope.observeArchivedUnreadConversationsCount - - @ViewModelScoped - @Provides - fun provideObserveUsersTypingUseCase(conversationScope: ConversationScope): ObserveUsersTypingUseCase = - conversationScope.observeUsersTyping - - @ViewModelScoped - @Provides - fun provideSendTypingEventUseCase(conversationScope: ConversationScope): SendTypingEventUseCase = conversationScope.sendTypingEvent - - @ViewModelScoped - @Provides - fun provideClearTypingEventsUseCase(conversationScope: ConversationScope): ClearUsersTypingEventsUseCase = - conversationScope.clearUsersTypingEvents - - @ViewModelScoped - @Provides - fun provideSetUserInformedAboutVerificationBeforeMessagingUseCase( - conversationScope: ConversationScope - ): SetUserInformedAboutVerificationUseCase = - conversationScope.setUserInformedAboutVerificationBeforeMessagingUseCase - - @ViewModelScoped - @Provides - fun provideObserveInformAboutVerificationBeforeMessagingFlagUseCase( - conversationScope: ConversationScope - ): ObserveDegradedConversationNotifiedUseCase = - conversationScope.observeInformAboutVerificationBeforeMessagingFlagUseCase - - @ViewModelScoped - @Provides - fun provideSetUserNotifiedAboutConversationUnderLegalHoldUseCase( - conversationScope: ConversationScope, - ): SetNotifiedAboutConversationUnderLegalHoldUseCase = conversationScope.setNotifiedAboutConversationUnderLegalHold - - @ViewModelScoped - @Provides - fun provideObserveLegalHoldWithChangeNotifiedForConversationUseCase( - conversationScope: ConversationScope, - ): ObserveConversationUnderLegalHoldNotifiedUseCase = conversationScope.observeConversationUnderLegalHoldNotified - - @ViewModelScoped - @Provides - fun provideSyncConversationCodeUseCase(conversationScope: ConversationScope): SyncConversationCodeUseCase = - conversationScope.syncConversationCode - - @ViewModelScoped - @Provides - fun provideIsOneToOneConversationCreatedUseCase(conversationScope: ConversationScope): IsOneToOneConversationCreatedUseCase = - conversationScope.isOneToOneConversationCreatedUseCase - - @ViewModelScoped - @Provides - fun provideGetConversationProtocolInfoUseCase(conversationScope: ConversationScope): GetConversationProtocolInfoUseCase = - conversationScope.getConversationProtocolInfo - - @ViewModelScoped - @Provides - fun provideGetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(conversationScope: ConversationScope) = - conversationScope.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery - - @ViewModelScoped - @Provides - fun provideObserveConversationsFromFolderUseCase(conversationScope: ConversationScope) = - conversationScope.observeConversationsFromFolder - - @ViewModelScoped - @Provides - fun provideGetFavoriteFolderUseCase(conversationScope: ConversationScope) = - conversationScope.getFavoriteFolder - - @ViewModelScoped - @Provides - fun provideAddConversationToFavoritesUseCase(conversationScope: ConversationScope) = - conversationScope.addConversationToFavorites - - @ViewModelScoped - @Provides - fun provideRemoveConversationFromFavoritesUseCase(conversationScope: ConversationScope) = - conversationScope.removeConversationFromFavorites - - @ViewModelScoped - @Provides - fun provideObserveUserFoldersUseCase(conversationScope: ConversationScope) = - conversationScope.observeUserFolders - - @ViewModelScoped - @Provides - fun provideMoveConversationToFolderUseCase(conversationScope: ConversationScope) = - conversationScope.moveConversationToFolder - - @ViewModelScoped - @Provides - fun provideRemoveConversationFromFolderUseCase(conversationScope: ConversationScope) = - conversationScope.removeConversationFromFolder - - @ViewModelScoped - @Provides - fun provideCreateConversationFolderUseCase(conversationScope: ConversationScope) = - conversationScope.createConversationFolder - - @ViewModelScoped - @Provides - fun provideResetMlsConversationUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): ResetMLSConversationUseCase = coreLogic.getSessionScope(currentAccount).resetMlsConversation - - @ViewModelScoped - @Provides - fun provideFetchConversationUseCase( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): FetchConversationUseCase = coreLogic.getSessionScope(currentAccount).fetchConversationUseCase - - @ViewModelScoped - @Provides - fun provideChangeAccessForAppsInConversationUseCase( - conversationScope: ConversationScope - ): ChangeAccessForAppsInConversationUseCase = - conversationScope.changeAccessForAppsInConversation -} +// Conversation account-scoped providers are owned by WireMetroGraph. diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt deleted file mode 100644 index c60703ddad7..00000000000 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Wire - * Copyright (C) 2025 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.di.accountScoped - -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.debug.BreakSessionUseCase -import com.wire.kalium.logic.feature.debug.DebugScope -import com.wire.kalium.logic.feature.debug.GetDebugE2EICertificateExpirationUseCase -import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase -import com.wire.kalium.logic.feature.debug.GetConversationCryptoStatsUseCase -import com.wire.kalium.logic.feature.debug.GetConversationEpochFromCCUseCase -import com.wire.kalium.logic.feature.debug.RepairFaultyRemovalKeysUseCase -import com.wire.kalium.logic.feature.debug.SetDebugE2EICertificateExpirationUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -@Suppress("TooManyFunctions") -class DebugModule { - - @ViewModelScoped - @Provides - fun providesDebugScope( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): DebugScope = coreLogic.getSessionScope(currentAccount).debug - - @ViewModelScoped - @Provides - fun provideDisableEventProcessing(debugScope: DebugScope) = - debugScope.disableEventProcessing - - @ViewModelScoped - @Provides - fun provideBreakSessionUseCase(debugScope: DebugScope): BreakSessionUseCase = - debugScope.breakSession - - @ViewModelScoped - @Provides - fun provideSendFCMTokenToAPIUseCase(debugScope: DebugScope) = - debugScope.sendFCMTokenToServer - - @ViewModelScoped - @Provides - fun provideChangeProfilingUseCase(debugScope: DebugScope) = - debugScope.changeProfiling - - @ViewModelScoped - @Provides - fun provideObserveDatabaseLoggerState(debugScope: DebugScope) = - debugScope.observeDatabaseLoggerState - - @ViewModelScoped - @Provides - fun provideObserveAsyncNotificationsEnabled(debugScope: DebugScope) = debugScope.observeIsConsumableNotificationsEnabled - - @ViewModelScoped - @Provides - fun provideStartUsingAsyncNotifications(debugScope: DebugScope) = debugScope.startUsingAsyncNotifications - - @ViewModelScoped - @Provides - fun provideFeatureConfigUseCase(debugScope: DebugScope): GetFeatureConfigUseCase = debugScope.getFeatureConfig - - @ViewModelScoped - @Provides - fun provideGetDebugE2EICertificateExpirationUseCase(debugScope: DebugScope): GetDebugE2EICertificateExpirationUseCase = - debugScope.getDebugE2EICertificateExpiration - - @ViewModelScoped - @Provides - fun provideSetDebugE2EICertificateExpirationUseCase(debugScope: DebugScope): SetDebugE2EICertificateExpirationUseCase = - debugScope.setDebugE2EICertificateExpiration - - @ViewModelScoped - @Provides - fun provideGetConversationEpochFromCCUseCase(debugScope: DebugScope): GetConversationEpochFromCCUseCase = - debugScope.getConversationEpochFromCC - - @ViewModelScoped - @Provides - fun provideDebugFeedConversationUseCase(debugScope: DebugScope) = - debugScope.debugFeedConversationUseCase - - @ViewModelScoped - @Provides - fun provideRepairFaultyRemovalKeysUseCase(debugScope: DebugScope): RepairFaultyRemovalKeysUseCase = - debugScope.repairFaultyRemovalKeysUseCase - - @ViewModelScoped - @Provides - fun provideGetConversationCryptoStatsUseCase(debugScope: DebugScope): GetConversationCryptoStatsUseCase = - debugScope.getConversationCryptoStats -} diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt index b0840f9b021..47cf0968893 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/MessageModule.kt @@ -17,246 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.asset.GetImageAssetMessagesForConversationUseCase -import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase -import com.wire.kalium.logic.feature.asset.GetPaginatedFlowOfAssetMessageByConversationIdUseCase -import com.wire.kalium.logic.feature.asset.ObserveAssetStatusesUseCase -import com.wire.kalium.logic.feature.asset.ObservePaginatedAssetImageMessages -import com.wire.kalium.logic.feature.asset.UpdateAssetMessageTransferStatusUseCase -import com.wire.kalium.logic.feature.asset.UpdateAudioMessageNormalizedLoudnessUseCase -import com.wire.kalium.logic.feature.asset.upload.ScheduleNewAssetMessageUseCase -import com.wire.kalium.logic.feature.incallreaction.SendInCallReactionUseCase -import com.wire.kalium.logic.feature.message.DeleteMessageUseCase -import com.wire.kalium.logic.feature.message.FetchOlderNomadMessagesByConversationUseCase -import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase -import com.wire.kalium.logic.feature.message.GetNotificationsUseCase -import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesByConversationUseCase -import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase -import com.wire.kalium.logic.feature.message.GetSearchedConversationMessagePositionUseCase -import com.wire.kalium.logic.feature.message.GetSenderNameByMessageIdUseCase -import com.wire.kalium.logic.feature.message.MarkMessagesAsNotifiedUseCase -import com.wire.kalium.logic.feature.message.MessageScope -import com.wire.kalium.logic.feature.message.ObserveMessageByIdUseCase -import com.wire.kalium.logic.feature.message.ObserveMessageReactionsUseCase -import com.wire.kalium.logic.feature.message.ObserveMessageReceiptsUseCase -import com.wire.kalium.logic.feature.message.RetryFailedMessageUseCase -import com.wire.kalium.logic.feature.message.SendEditMultipartMessageUseCase -import com.wire.kalium.logic.feature.message.SendEditTextMessageUseCase -import com.wire.kalium.logic.feature.message.SendKnockUseCase -import com.wire.kalium.logic.feature.message.SendLocationUseCase -import com.wire.kalium.logic.feature.message.SendMultipartMessageUseCase -import com.wire.kalium.logic.feature.message.SendTextMessageUseCase -import com.wire.kalium.logic.feature.message.ToggleReactionUseCase -import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase -import com.wire.kalium.logic.feature.message.draft.GetMessageDraftUseCase -import com.wire.kalium.logic.feature.message.draft.RemoveMessageDraftUseCase -import com.wire.kalium.logic.feature.message.draft.SaveMessageDraftUseCase -import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase -import com.wire.kalium.logic.feature.message.fetchOlderMessagesByConversationId -import com.wire.kalium.logic.feature.message.getPaginatedFlowOfAssetMessageByConversationId -import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesByConversation -import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesBySearchQueryAndConversation -import com.wire.kalium.logic.feature.message.observePaginatedImageAssetMessageByConversationId -import com.wire.kalium.logic.feature.sessionreset.ResetSessionUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -@Suppress("TooManyFunctions") -class MessageModule { - - @ViewModelScoped - @Provides - fun provideMessageScope( - @CurrentAccount currentAccount: UserId, - @KaliumCoreLogic coreLogic: CoreLogic - ): MessageScope = coreLogic.getSessionScope(currentAccount).messages - - @ViewModelScoped - @Provides - fun provideSendButtonActionMessageUseCase(messageScope: MessageScope): SendButtonActionMessageUseCase = - messageScope.sendButtonActionMessage - - @ViewModelScoped - @Provides - fun provideEnqueueMessageSelfDeletionUseCase(messageScope: MessageScope): EnqueueMessageSelfDeletionUseCase = - messageScope.enqueueMessageSelfDeletion - - @ViewModelScoped - @Provides - fun provideResetSessionUseCase(messageScope: MessageScope): ResetSessionUseCase = - messageScope.resetSession - - @ViewModelScoped - @Provides - fun provideDeleteMessageUseCase(messageScope: MessageScope): DeleteMessageUseCase = - messageScope.deleteMessage - - @ViewModelScoped - @Provides - fun provideMarkMessagesAsNotifiedUseCase(messageScope: MessageScope): MarkMessagesAsNotifiedUseCase = - messageScope.markMessagesAsNotified - - @ViewModelScoped - @Provides - fun provideUpdateAssetMessageTransferStatusUseCase(messageScope: MessageScope): UpdateAssetMessageTransferStatusUseCase = - messageScope.updateAssetMessageTransferStatus - - @ViewModelScoped - @Provides - fun provideSendTextMessageUseCase(messageScope: MessageScope): SendTextMessageUseCase = messageScope.sendTextMessage - - @ViewModelScoped - @Provides - fun provideSendEditTextMessageUseCase(messageScope: MessageScope): SendEditTextMessageUseCase = - messageScope.sendEditTextMessage - - @ViewModelScoped - @Provides - fun provideSendEditMultipartMessageUseCase(messageScope: MessageScope): SendEditMultipartMessageUseCase = - messageScope.sendEditMultipartMessage - - @ViewModelScoped - @Provides - fun provideRetryFailedMessageUseCase(messageScope: MessageScope): RetryFailedMessageUseCase = - messageScope.retryFailedMessage - - @ViewModelScoped - @Provides - fun provideSendKnockUseCase(messageScope: MessageScope): SendKnockUseCase = - messageScope.sendKnock - - @ViewModelScoped - @Provides - fun provideToggleReactionUseCase(messageScope: MessageScope): ToggleReactionUseCase = - messageScope.toggleReaction - - @ViewModelScoped - @Provides - fun provideObserveMessageReactionsUseCase(messageScope: MessageScope): ObserveMessageReactionsUseCase = - messageScope.observeMessageReactions - - @ViewModelScoped - @Provides - fun provideObserveMessageReceiptsUseCase(messageScope: MessageScope): ObserveMessageReceiptsUseCase = - messageScope.observeMessageReceipts - - @ViewModelScoped - @Provides - fun providesSendAssetMessageUseCase(messageScope: MessageScope): ScheduleNewAssetMessageUseCase = - messageScope.sendAssetMessage - - @ViewModelScoped - @Provides - fun provideGetPrivateAssetUseCase(messageScope: MessageScope): GetMessageAssetUseCase = - messageScope.getAssetMessage - - @ViewModelScoped - @Provides - fun provideGetNotificationsUseCase(messageScope: MessageScope): GetNotificationsUseCase = - messageScope.getNotifications - - @ViewModelScoped - @Provides - fun provideGetMessageByIdUseCase(messageScope: MessageScope): GetMessageByIdUseCase = - messageScope.getMessageById - - @ViewModelScoped - @Provides - fun provideObserveMessageByIdUseCase(messageScope: MessageScope): ObserveMessageByIdUseCase = - messageScope.observeMessageById - - @ViewModelScoped - @Provides - fun provideGetPaginatedMessagesUseCase(messageScope: MessageScope): GetPaginatedFlowOfMessagesByConversationUseCase = - messageScope.getPaginatedFlowOfMessagesByConversation - - @ViewModelScoped - @Provides - fun provideFetchOlderMessagesUseCase(messageScope: MessageScope): FetchOlderNomadMessagesByConversationUseCase = - messageScope.fetchOlderMessagesByConversationId - - @ViewModelScoped - @Provides - fun provideGetImageAssetMessagesByConversationUseCase(messageScope: MessageScope): GetImageAssetMessagesForConversationUseCase = - messageScope.getImageAssetMessagesByConversation - - @ViewModelScoped - @Provides - fun provideGetPaginatedFlowOfAssetMessageByConversationId( - messageScope: MessageScope - ): GetPaginatedFlowOfAssetMessageByConversationIdUseCase = - messageScope.getPaginatedFlowOfAssetMessageByConversationId - - @ViewModelScoped - @Provides - fun provideGetPaginatedFlowOfImageAssetMessageByConversationId( - messageScope: MessageScope - ): ObservePaginatedAssetImageMessages = - messageScope.observePaginatedImageAssetMessageByConversationId - - @ViewModelScoped - @Provides - fun provideGetPaginatedFlowOfMessagesBySearchQueryAndConversation( - messageScope: MessageScope - ): GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase = - messageScope.getPaginatedFlowOfMessagesBySearchQueryAndConversation - - @ViewModelScoped - @Provides - fun provideGetSearchedConversationMessagePositionUseCase(messageScope: MessageScope): GetSearchedConversationMessagePositionUseCase = - messageScope.getSearchedConversationMessagePosition - - @ViewModelScoped - @Provides - fun provideSendLocationUseCase(messageScope: MessageScope): SendLocationUseCase = - messageScope.sendLocation - - @ViewModelScoped - @Provides - fun provideObserveAssetStatusesUseCase(messageScope: MessageScope): ObserveAssetStatusesUseCase = - messageScope.observeAssetStatuses - - @ViewModelScoped - @Provides - fun provideSaveMessageDraftUseCase(messageScope: MessageScope): SaveMessageDraftUseCase = - messageScope.saveMessageDraftUseCase - - @ViewModelScoped - @Provides - fun provideGetMessageDraftUseCase(messageScope: MessageScope): GetMessageDraftUseCase = - messageScope.getMessageDraftUseCase - - @ViewModelScoped - @Provides - fun provideRemoveMessageDraftUseCase(messageScope: MessageScope): RemoveMessageDraftUseCase = - messageScope.removeMessageDraftUseCase - - @ViewModelScoped - @Provides - fun provideSendInCallReactionUseCase(messageScope: MessageScope): SendInCallReactionUseCase = - messageScope.sendInCallReactionUseCase - - @ViewModelScoped - @Provides - fun provideGetSenderNameByMessageIdUseCase(messageScope: MessageScope): GetSenderNameByMessageIdUseCase = - messageScope.getSenderNameByMessageId - - @ViewModelScoped - @Provides - fun provideSendMultipartMessageUseCase(messageScope: MessageScope): SendMultipartMessageUseCase = - messageScope.sendMultipartMessage - - @ViewModelScoped - @Provides - fun provideUpdateAudioMessageNormalizedLoudnessUseCase(messageScope: MessageScope): UpdateAudioMessageNormalizedLoudnessUseCase = - messageScope.updateAudioMessageNormalizedLoudnessUseCase -} +// Message account-scoped providers are owned by WireMetroGraph. diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt index e5bd7ef34a8..9dd82eeab13 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/SearchModule.kt @@ -16,47 +16,3 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.wire.android.di.accountScoped - -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.search.FederatedSearchParser -import com.wire.kalium.logic.feature.search.IsFederationSearchAllowedUseCase -import com.wire.kalium.logic.feature.search.SearchByHandleUseCase -import com.wire.kalium.logic.feature.search.SearchScope -import com.wire.kalium.logic.feature.search.SearchUsersUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -class SearchModule { - - @ViewModelScoped - @Provides - fun provideSearchScope( - @CurrentAccount currentAccount: UserId, - @KaliumCoreLogic coreLogic: CoreLogic - ): SearchScope = coreLogic.getSessionScope(currentAccount).search - - @ViewModelScoped - @Provides - fun provideSearchUsersUseCase(searchScope: SearchScope): SearchUsersUseCase = searchScope.searchUsers - - @ViewModelScoped - @Provides - fun provideSearchByHandleUseCase(searchScope: SearchScope): SearchByHandleUseCase = searchScope.searchByHandle - - @ViewModelScoped - @Provides - fun provideFederatedSearchParser(searchScope: SearchScope): FederatedSearchParser = searchScope.federatedSearchParser - - @ViewModelScoped - @Provides - fun provideIsFederationSearchAllowedUseCase(searchScope: SearchScope): IsFederationSearchAllowedUseCase = - searchScope.isFederationSearchAllowedUseCase -} 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 deleted file mode 100644 index f3b7c07adae..00000000000 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/ServicesModule.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.android.di.accountScoped - -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.service.GetServiceByIdUseCase -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 -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -class ServicesModule { - - @ViewModelScoped - @Provides - fun provideServiceScope( - @CurrentAccount currentAccount: UserId, - @KaliumCoreLogic coreLogic: CoreLogic - ): ServiceScope = coreLogic.getSessionScope(currentAccount).service - - @ViewModelScoped - @Provides - fun provideObserveIsServiceMemberUseCase(serviceScope: ServiceScope): ObserveIsServiceMemberUseCase = - serviceScope.observeIsServiceMember - - @ViewModelScoped - @Provides - fun provideGetServiceByIdUseCase(serviceScope: ServiceScope): GetServiceByIdUseCase = - serviceScope.getServiceById - - @ViewModelScoped - @Provides - fun provideObserveAllServicesUseCase(serviceScope: ServiceScope): ObserveAllServicesUseCase = - serviceScope.observeAllServices - - @ViewModelScoped - @Provides - fun provideSyncServicesUseCase(serviceScope: ServiceScope): SyncServicesUseCase = - serviceScope.syncServices - - @ViewModelScoped - @Provides - fun provideSearchServicesByNameUseCase(serviceScope: ServiceScope): SearchServicesByNameUseCase = - serviceScope.searchServicesByName - - @ViewModelScoped - @Provides - fun provideObserveIsAppsAllowedForUsage(serviceScope: ServiceScope) = - serviceScope.observeIsAppsAllowedForUsage -} diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/TeamModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/TeamModule.kt index af8ff4cf1db..c5e0f030820 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/TeamModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/TeamModule.kt @@ -17,37 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.team.SyncSelfTeamInfoUseCase -import com.wire.kalium.logic.feature.team.TeamScope -import com.wire.kalium.logic.feature.user.IsSelfATeamMemberUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -class TeamModule { - - @ViewModelScoped - @Provides - fun provideTeamScope( - @CurrentAccount currentAccount: UserId, - @KaliumCoreLogic coreLogic: CoreLogic - ): TeamScope = coreLogic.getSessionScope(currentAccount).team - - @ViewModelScoped - @Provides - fun provideSyncSelfTeamInfoUseCase(teamScope: TeamScope): SyncSelfTeamInfoUseCase = - teamScope.syncSelfTeamInfoUseCase - - @ViewModelScoped - @Provides - fun provideIsSelfATeamMemberUseCase(teamScope: TeamScope): IsSelfATeamMemberUseCase = - teamScope.isSelfATeamMember -} +// Account-scoped team bindings are provided by the Metro graph. diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt index 30d55be61ce..f6c8a99354b 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/UserModule.kt @@ -17,261 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.asset.DeleteAssetUseCase -import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase -import com.wire.kalium.logic.feature.asset.GetAvatarAssetUseCase -import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment -import com.wire.kalium.logic.feature.client.IsProfileQRCodeEnabledUseCase -import com.wire.kalium.logic.feature.client.IsWireCellsEnabledForConversationUseCase -import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase -import com.wire.kalium.logic.feature.conversation.GetAllContactsNotInConversationUseCase -import com.wire.kalium.logic.feature.e2ei.usecase.GetMLSClientIdentityUseCase -import com.wire.kalium.logic.feature.e2ei.usecase.GetMembersE2EICertificateStatusesUseCase -import com.wire.kalium.logic.feature.e2ei.usecase.GetUserMlsClientIdentitiesUseCase -import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase -import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase -import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCase -import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCase -import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase -import com.wire.kalium.logic.feature.user.DeleteAccountUseCase -import com.wire.kalium.logic.feature.user.GetSelfUserUseCase -import com.wire.kalium.logic.feature.user.GetUserInfoUseCase -import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase -import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase -import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase -import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase -import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase -import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase -import com.wire.kalium.logic.feature.user.SetUserHandleUseCase -import com.wire.kalium.logic.feature.user.UpdateAccentColorUseCase -import com.wire.kalium.logic.feature.user.UpdateDisplayNameUseCase -import com.wire.kalium.logic.feature.user.UpdateEmailUseCase -import com.wire.kalium.logic.feature.user.UpdateSelfAvailabilityStatusUseCase -import com.wire.kalium.logic.feature.user.UploadUserAvatarUseCase -import com.wire.kalium.logic.feature.user.UserScope -import com.wire.kalium.logic.feature.user.readReceipts.ObserveReadReceiptsEnabledUseCase -import com.wire.kalium.logic.feature.user.readReceipts.PersistReadReceiptsStatusConfigUseCase -import com.wire.kalium.logic.feature.user.typingIndicator.ObserveTypingIndicatorEnabledUseCase -import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicatorStatusConfigUseCase -import com.wire.kalium.logic.sync.ForegroundActionsUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -@Suppress("TooManyFunctions") -class UserModule { - - @Provides - @ViewModelScoped - fun provideUserScope( - @KaliumCoreLogic coreLogic: CoreLogic, - @CurrentAccount currentAccount: UserId - ): UserScope = coreLogic.getSessionScope(currentAccount).users - - @ViewModelScoped - @Provides - fun provideRefreshUsersWithoutMetadataUseCase( - userScope: UserScope - ): RefreshUsersWithoutMetadataUseCase = userScope.refreshUsersWithoutMetadata - - @ViewModelScoped - @Provides - fun provideDeleteAccountUseCase( - userScope: UserScope - ): DeleteAccountUseCase = - userScope.deleteAccount - - @ViewModelScoped - @Provides - fun provideUpdateEmailUseCase( - userScope: UserScope - ): UpdateEmailUseCase = - userScope.updateEmail - - @ViewModelScoped - @Provides - fun provideUpdateDisplayNameUseCase( - userScope: UserScope - ): UpdateDisplayNameUseCase = - userScope.updateDisplayName - - @ViewModelScoped - @Provides - fun provideUpdateAccentColorUseCase( - userScope: UserScope - ): UpdateAccentColorUseCase = - userScope.updateAccentColor - - @ViewModelScoped - @Provides - fun provideGetAssetSizeLimitUseCase( - userScope: UserScope - ): GetAssetSizeLimitUseCase = - userScope.getAssetSizeLimit - - @ViewModelScoped - @Provides - fun provideObserveReadReceiptsEnabled(userScope: UserScope): ObserveReadReceiptsEnabledUseCase = - userScope.observeReadReceiptsEnabled - - @ViewModelScoped - @Provides - fun providePersistReadReceiptsStatusConfig(userScope: UserScope): PersistReadReceiptsStatusConfigUseCase = - userScope.persistReadReceiptsStatusConfig - - @ViewModelScoped - @Provides - fun provideFinalizeMLSClientAfterE2EIEnrollmentUseCase(userScope: UserScope): FinalizeMLSClientAfterE2EIEnrollment = - userScope.finalizeMLSClientAfterE2EIEnrollment - - @ViewModelScoped - @Provides - fun provideObserveTypingIndicatorEnabled(userScope: UserScope): ObserveTypingIndicatorEnabledUseCase = - userScope.observeTypingIndicatorEnabled - - @ViewModelScoped - @Provides - fun providePersistTypingIndicatorStatusConfig(userScope: UserScope): PersistTypingIndicatorStatusConfigUseCase = - userScope.persistTypingIndicatorStatusConfig - - @ViewModelScoped - @Provides - fun provideSelfServerConfig( - userScope: UserScope - ): SelfServerConfigUseCase = userScope.serverLinks - - @ViewModelScoped - @Provides - fun provideObserveUserInfoUseCase( - userScope: UserScope - ): ObserveUserInfoUseCase = userScope.observeUserInfo - - @ViewModelScoped - @Provides - fun provideIsPasswordRequiredUseCase( - userScope: UserScope - ): IsPasswordRequiredUseCase = userScope.isPasswordRequired - - @ViewModelScoped - @Provides - fun provideIsReadOnlyAccountUseCase( - userScope: UserScope - ): IsReadOnlyAccountUseCase = userScope.isReadOnlyAccount - - @ViewModelScoped - @Provides - fun provideGetAllContactsNotInTheConversationUseCase( - userScope: UserScope - ): GetAllContactsNotInConversationUseCase = - userScope.getAllContactsNotInConversation - - @ViewModelScoped - @Provides - fun provideGetUserInfoUseCase(userScope: UserScope): GetUserInfoUseCase = - userScope.getUserInfo - - @ViewModelScoped - @Provides - fun provideUpdateSelfAvailabilityStatusUseCase(userScope: UserScope): UpdateSelfAvailabilityStatusUseCase = - userScope.updateSelfAvailabilityStatus - - @ViewModelScoped - @Provides - fun provideGetAllContactsUseCase( - userScope: UserScope - ): GetAllContactsUseCase = - userScope.getAllKnownUsers - - @ViewModelScoped - @Provides - fun provideGetKnownUserUseCase( - userScope: UserScope - ): GetKnownUserUseCase = - userScope.getKnownUser - - @ViewModelScoped - @Provides - fun provideGetSelfUseCase(userScope: UserScope): GetSelfUserUseCase = - userScope.getSelfUser - - @ViewModelScoped - @Provides - fun provideObserveSelfUseCase(userScope: UserScope): ObserveSelfUserUseCase = - userScope.observeSelfUser - - @ViewModelScoped - @Provides - fun provideObserveSelfUserWithTeamUseCase(userScope: UserScope): ObserveSelfUserWithTeamUseCase = - userScope.observeSelfUserWithTeam - - @ViewModelScoped - @Provides - fun provideGetAvatarAssetUseCase(userScope: UserScope): GetAvatarAssetUseCase = - userScope.getPublicAsset - - @ViewModelScoped - @Provides - fun provideDeleteAssetUseCase(userScope: UserScope): DeleteAssetUseCase = - userScope.deleteAsset - - @ViewModelScoped - @Provides - fun provideUploadUserAvatarUseCase(userScope: UserScope): UploadUserAvatarUseCase = - userScope.uploadUserAvatar - - @ViewModelScoped - @Provides - fun provideSetUserHandleUseCase(userScope: UserScope): SetUserHandleUseCase = - userScope.setUserHandle - - @ViewModelScoped - @Provides - fun provideGetE2EICertificateUseCase(userScope: UserScope): GetMLSClientIdentityUseCase = - userScope.getE2EICertificate - - @ViewModelScoped - @Provides - fun provideGetUserE2eiCertificateStatusUseCase(userScope: UserScope): IsOtherUserE2EIVerifiedUseCase = - userScope.getUserE2eiCertificateStatus - - @ViewModelScoped - @Provides - fun provideGetMembersE2EICertificateStatusesUseCase(userScope: UserScope): GetMembersE2EICertificateStatusesUseCase = - userScope.getMembersE2EICertificateStatuses - - @ViewModelScoped - @Provides - fun provideGetUserMlsClientIdentities(userScope: UserScope): GetUserMlsClientIdentitiesUseCase = - userScope.getUserMlsClientIdentities - - @ViewModelScoped - @Provides - fun provideIsPersonalToTeamAccountSupportedByBackendUseCase(userScope: UserScope): CanMigrateFromPersonalToTeamUseCase = - userScope.isPersonalToTeamAccountSupportedByBackend - - @ViewModelScoped - @Provides - fun provideForegroundActionsUseCase(userScope: UserScope): ForegroundActionsUseCase = userScope.foregroundActions - - @ViewModelScoped - @Provides - fun provideCellsConfigUseCase(userScope: UserScope): IsWireCellsEnabledUseCase = userScope.isWireCellsEnabled - - @ViewModelScoped - @Provides - fun provideIsWireCellsEnabledForConversationUseCase(userScope: UserScope): IsWireCellsEnabledForConversationUseCase = - userScope.isWireCellsEnabledForConversation - - @ViewModelScoped - @Provides - fun provideProfileQRCodeConfigUseCase(userScope: UserScope): IsProfileQRCodeEnabledUseCase = - userScope.isProfileQRCodeEnabled -} +// Account-scoped user bindings are provided by the Metro graph. diff --git a/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelExt.kt b/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelExt.kt new file mode 100644 index 00000000000..9069e49d9a2 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelExt.kt @@ -0,0 +1,54 @@ +/* + * 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.di.metro + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory + +@Composable +inline fun metroViewModel( + viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + }, + key: String? = null, + crossinline create: WireMetroGraph.() -> VM, +): VM { + val providedGraph = LocalMetroViewModelGraph.current as? WireMetroGraph + val context = LocalContext.current + val graph = providedGraph ?: remember(context) { createWireMetroGraph(context) } + val factory = remember(graph) { + viewModelFactory { + initializer { + graph.create() + } + } + } + return viewModel( + modelClass = VM::class, + viewModelStoreOwner = viewModelStoreOwner, + key = key, + factory = factory, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelMigrationTemplate.kt b/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelMigrationTemplate.kt new file mode 100644 index 00000000000..a1de35ed068 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelMigrationTemplate.kt @@ -0,0 +1,87 @@ +/* + * 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.di.metro + +/** + * Template for migrating Android ViewModels toward the Metro + native iOS shape proven in WireOne. + * + * The migration should move creation responsibility out of Android-specific APIs while keeping current Android runtime + * creation on the current Android runtime until the Metro graph is wired into the app. Prefer this shape: + * + * ``` + * @Inject + * class ExampleViewModelFactory( + * private val dependency: Dependency, + * private val lazyFeature: Provider, + * ) { + * fun create( + * args: ExampleArgs, + * navigator: ExampleNavigator, + * flowStateHolder: ExampleFlowStateHolder, + * coroutineScope: CoroutineScope? = null, + * ): ExampleViewModel = ExampleViewModel( + * args = args, + * navigator = navigator, + * flowStateHolder = flowStateHolder, + * dependency = dependency, + * lazyFeature = lazyFeature, + * coroutineScope = coroutineScope, + * ) + * } + * ``` + * + * Rules for each migrated ViewModel: + * - keep the ViewModel constructor platform-neutral: no `SavedStateHandle`, `NavController`, Compose destination args, + * Android `Context`, or direct platform-only creation contract; + * - keep runtime/session args explicit in `create(...)`, especially values previously pulled from navigation state; + * - keep long-lived dependencies injected into the factory by Metro; + * - use `Provider` for dependencies that can be cyclic or should stay lazy; + * - pass a nullable/testable `CoroutineScope` only when the ViewModel already supports external scope injection; + * - keep Android behavior unchanged while both Android and Metro creation coexist. + * + * For iOS, add a small bridge next to the feature instead of exporting Android lifecycle concepts: + * + * ``` + * fun createExampleIosViewModel( + * navigator: ExampleNavigator, + * flowStateHolder: ExampleFlowStateHolder, + * exampleViewModelFactory: ExampleViewModelFactory, + * ): IosViewModel { + * val vm = exampleViewModelFactory.create( + * navigator = navigator, + * flowStateHolder = flowStateHolder, + * args = ExampleArgs(...), + * ) + * + * return IosViewModel( + * state = vm.state, + * effects = vm.effects, + * onIntent = vm::sendIntent, + * ) + * } + * ``` + * + * For flows spanning multiple screens, mirror WireOne's login flow: + * - create a feature-level navigator abstraction for screen-to-screen transitions; + * - create a feature-level state holder for values shared across steps; + * - create all step ViewModels from factories using the same navigator/state holder; + * - expose the iOS bridge from the Metro graph as graph properties, not through Android screens. + * + * Do not enable Metro Dagger interop for this pass. Existing Android runtime creation stays until the bridge is ready. + */ +internal object MetroViewModelMigrationTemplate diff --git a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt new file mode 100644 index 00000000000..4ef2a8f6aa4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -0,0 +1,3280 @@ +/* + * 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.di.metro + +import android.app.NotificationManager +import android.content.Context +import android.location.Geocoder +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.MediaPlayer +import android.os.Build +import androidx.lifecycle.SavedStateHandle +import androidx.core.app.NotificationManagerCompat +import androidx.work.WorkManager +import com.wire.android.BuildConfig +import com.wire.android.GlobalObserversManager +import com.wire.android.config.NomadProfilesFeatureConfig +import com.wire.android.config.ServerConfigProvider +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.datastore.UserDataStore +import com.wire.android.datastore.UserDataStoreProvider +import com.wire.android.di.ApplicationScope +import com.wire.android.di.ClientScopeProvider +import com.wire.android.di.CurrentAccount +import com.wire.android.di.DefaultWebSocketEnabledByDefault +import com.wire.android.di.IsProfileQRCodeEnabledUseCaseProvider +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.di.ObserveIfE2EIRequiredDuringLoginUseCaseProvider +import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider +import com.wire.android.di.ObserveSelfUserUseCaseProvider +import com.wire.android.di.ObserveSyncStateUseCaseProvider +import com.wire.android.emm.AndroidUserContextProvider +import com.wire.android.emm.AndroidUserContextProviderImpl +import com.wire.android.emm.ManagedConfigParser +import com.wire.android.emm.ManagedConfigParserImpl +import com.wire.android.emm.ManagedConfigurationsManager +import com.wire.android.emm.ManagedConfigurationsManagerImpl +import com.wire.android.emm.ManagedConfigurationsReceiver +import com.wire.android.emm.ManagedConfigurationsReporter +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.feature.DisableAppLockUseCase +import com.wire.android.feature.ObserveAppLockConfigUseCase +import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl +import com.wire.android.feature.cells.ui.AndroidCellFileExternalActions +import com.wire.android.feature.cells.ui.CellFileExternalActions +import com.wire.android.media.audiomessage.AudioMessageViewModelFactory +import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer +import com.wire.android.media.CallRinger +import com.wire.android.media.PingRinger +import com.wire.android.mapper.MessageContentMapper +import com.wire.android.mapper.MessageMapper +import com.wire.android.mapper.MessageResourceProvider +import com.wire.android.mapper.OtherAccountMapper +import com.wire.android.mapper.ContactMapper +import com.wire.android.mapper.RegularMessageMapper +import com.wire.android.mapper.SystemMessageContentMapper +import com.wire.android.mapper.UIAssetMapper +import com.wire.android.mapper.UICallParticipantMapper +import com.wire.android.mapper.UIParticipantMapper +import com.wire.android.mapper.UserTypeMapper +import com.wire.android.model.ImageAssetViewModelFactory +import com.wire.android.model.ImageAssetViewModelGraph +import com.wire.android.notification.WireNotificationManager +import com.wire.android.notification.CallNotificationManager +import com.wire.android.notification.CallNotificationBuilder +import com.wire.android.notification.MessageNotificationManager +import com.wire.android.notification.NotificationChannelsManager +import com.wire.android.notification.broadcastreceivers.DynamicReceiversManager +import com.wire.android.navigation.LoginTypeSelector +import com.wire.android.services.CallService +import com.wire.android.services.CallServiceManager +import com.wire.android.services.PersistentWebSocketService +import com.wire.android.services.PlayingAudioMessageService +import com.wire.android.services.ServicesManager +import com.wire.android.sync.MonitorSyncWorkUseCase +import com.wire.android.ui.AndroidWireActivityIntentGateway +import com.wire.android.ui.CallFeedbackViewModelFactory +import com.wire.android.ui.WireActivityViewModelFactory +import com.wire.android.ui.WireActivityIntentGateway +import com.wire.android.ui.calling.CallActivityViewModelFactory +import com.wire.android.ui.calling.common.ProximitySensorManager +import com.wire.android.ui.common.banner.SecurityClassificationViewModelFactory +import com.wire.android.ui.common.bottomsheet.conversation.ConversationOptionsMenuViewModelFactory +import com.wire.android.ui.authentication.create.code.CreateAccountCodeViewModelFactory +import com.wire.android.ui.authentication.create.details.CreateAccountDetailsViewModelFactory +import com.wire.android.ui.authentication.create.email.CreateAccountEmailViewModelFactory +import com.wire.android.ui.authentication.create.overview.CreateAccountOverviewViewModelFactory +import com.wire.android.ui.authentication.create.summary.CreateAccountSummaryViewModelFactory +import com.wire.android.ui.authentication.create.username.CreateAccountUsernameViewModelFactory +import com.wire.android.ui.authentication.devices.common.ClearSessionViewModelFactory +import com.wire.android.ui.authentication.devices.remove.RemoveDeviceViewModelFactory +import com.wire.android.ui.authentication.devices.register.RegisterDeviceViewModelFactory +import com.wire.android.ui.authentication.login.LoginSavedInputStore +import com.wire.android.ui.authentication.login.SavedStateLoginSavedInputStore +import com.wire.android.ui.authentication.login.email.LoginEmailViewModelFactory +import com.wire.android.ui.authentication.login.sso.LoginSSOSessionExceptionClassifier +import com.wire.android.ui.authentication.login.sso.LoginSSOViewModelFactory +import com.wire.android.ui.authentication.welcome.WelcomeViewModelFactory +import com.wire.android.ui.debug.AndroidExportObfuscatedCopyFileGateway +import com.wire.android.ui.debug.AndroidDebugDataInfoProvider +import com.wire.android.ui.debug.DebugDataInfoProvider +import com.wire.android.ui.debug.DebugDataOptionsViewModelFactory +import com.wire.android.ui.debug.ExportObfuscatedCopyFileGateway +import com.wire.android.ui.debug.ExportObfuscatedCopyViewModelFactory +import com.wire.android.ui.debug.LogManagementViewModelFactory +import com.wire.android.ui.debug.UserDebugViewModelFactory +import com.wire.android.ui.debug.cryptostats.ConversationCryptoStatsViewModelFactory +import com.wire.android.ui.debug.conversation.DebugConversationViewModelFactory +import com.wire.android.ui.debug.featureflags.DebugFeatureFlagsViewModelFactory +import com.wire.android.ui.e2eiEnrollment.E2EIEnrollmentViewModelFactory +import com.wire.android.ui.e2eiEnrollment.GetE2EICertificateViewModelFactory +import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModelFactory +import com.wire.android.ui.home.appLock.forgot.ForgotLockScreenViewModelFactory +import com.wire.android.ui.home.appLock.CurrentTimestampProvider +import com.wire.android.ui.home.appLock.LockCodeTimeManager +import com.wire.android.ui.home.appLock.set.SetLockScreenViewModelFactory +import com.wire.android.ui.home.appLock.unlock.AppUnlockWithBiometricsViewModelFactory +import com.wire.android.ui.home.appLock.unlock.EnterLockScreenViewModelFactory +import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveConversationRoleForUserUseCase +import com.wire.android.ui.home.conversations.details.participants.GroupConversationParticipantsViewModelFactory +import com.wire.android.ui.home.conversations.details.metadata.EditConversationMetadataViewModelFactory +import com.wire.android.ui.home.conversations.details.editselfdeletingmessages.EditSelfDeletingMessagesViewModelFactory +import com.wire.android.ui.home.conversations.details.editguestaccess.EditGuestAccessViewModelFactory +import com.wire.android.ui.home.conversations.details.editguestaccess.createPasswordProtectedGuestLink.CreatePasswordGuestLinkViewModelFactory +import com.wire.android.ui.home.conversations.details.GroupConversationDetailsViewModelFactory +import com.wire.android.ui.home.conversations.attachment.MessageAttachmentAssetImporter +import com.wire.android.ui.home.conversations.attachment.MessageAttachmentAssetImporterImpl +import com.wire.android.ui.home.conversations.attachment.MessageAttachmentFileGateway +import com.wire.android.ui.home.conversations.attachment.MessageAttachmentFileGatewayImpl +import com.wire.android.ui.home.conversations.attachment.MessageAttachmentsViewModelFactory +import com.wire.android.ui.home.conversations.banner.ConversationBannerViewModelFactory +import com.wire.android.ui.home.conversations.call.ConversationCallViewModelFactory +import com.wire.android.ui.home.conversations.composer.AndroidTempWritableAttachmentUriProvider +import com.wire.android.ui.home.conversations.composer.MessageComposerViewModelFactory +import com.wire.android.ui.home.conversations.composer.TempWritableAttachmentUriProvider +import com.wire.android.ui.home.conversations.folder.ConversationFoldersViewModelFactory +import com.wire.android.ui.home.conversations.folder.MoveConversationToFolderViewModelFactory +import com.wire.android.ui.home.conversations.folder.NewFolderViewModelFactory +import com.wire.android.ui.home.conversations.info.ConversationInfoViewModelFactory +import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase +import com.wire.android.ui.home.conversations.details.updateappsaccess.UpdateAppsAccessViewModelFactory +import com.wire.android.ui.home.conversations.details.updatechannelaccess.UpdateChannelAccessViewModelFactory +import com.wire.android.ui.home.conversations.edit.MessageOptionsMenuViewModelFactory +import com.wire.android.ui.home.conversations.messagedetails.MessageDetailsViewModelFactory +import com.wire.android.ui.home.conversations.media.preview.ImagesPreviewAssetImporter +import com.wire.android.ui.home.conversations.media.preview.ImagesPreviewAssetImporterImpl +import com.wire.android.ui.home.conversations.media.preview.ImagesPreviewViewModelFactory +import com.wire.android.ui.home.conversations.media.ConversationAssetMessagesViewModelFactory +import com.wire.android.ui.home.conversations.CompositeMessageViewModelFactory +import com.wire.android.ui.home.conversations.messages.AndroidConversationAssetFileGateway +import com.wire.android.ui.home.conversations.messages.ConversationAssetFileGateway +import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewModelFactory +import com.wire.android.ui.home.conversations.messages.QuotedMultipartMessageViewModelFactory +import com.wire.android.ui.home.conversations.messages.item.AssetLocalPathViewModelFactory +import com.wire.android.ui.home.conversations.messages.item.ConversationAssetPathsViewModelFactory +import com.wire.android.ui.home.conversations.messages.draft.MessageDraftViewModelFactory +import com.wire.android.ui.home.conversations.model.messagetypes.multipart.CellAssetRefreshHelper +import com.wire.android.ui.home.conversations.model.messagetypes.multipart.MultipartAttachmentsViewModelFactory +import com.wire.android.ui.home.conversations.migration.ConversationMigrationViewModelFactory +import com.wire.android.ui.home.conversations.promoteadmin.PromoteAdminViewModelFactory +import com.wire.android.ui.home.conversations.search.SearchUserViewModelFactory +import com.wire.android.ui.home.conversations.search.adddembertoconversation.AddMembersToConversationViewModelFactory +import com.wire.android.ui.home.conversations.search.apps.SearchAppsViewModelFactory +import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesViewModelFactory +import com.wire.android.ui.home.conversations.sendmessage.SendMessageViewModelFactory +import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase +import com.wire.android.ui.home.conversations.typing.TypingIndicatorViewModelFactory +import com.wire.android.ui.home.AppSyncViewModelFactory +import com.wire.android.ui.home.conversationslist.ConversationListCallViewModelFactory +import com.wire.android.ui.home.conversationslist.ConversationListViewModelFactory +import com.wire.android.ui.home.gallery.MediaGalleryViewModelFactory +import com.wire.android.ui.home.HomeViewModelFactory +import com.wire.android.ui.home.drawer.HomeDrawerViewModelFactory +import com.wire.android.ui.home.newconversation.NewConversationViewModelFactory +import com.wire.android.ui.home.messagecomposer.attachments.IsFileSharingEnabledViewModelFactory +import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionViewModelFactory +import com.wire.android.ui.home.messagecomposer.location.LocationPickerParameters +import com.wire.android.ui.home.messagecomposer.location.LocationPickerViewModelFactory +import com.wire.android.ui.home.messagecomposer.recordaudio.AndroidRecordAudioFileGateway +import com.wire.android.ui.home.messagecomposer.recordaudio.AudioMediaRecorder +import com.wire.android.ui.home.messagecomposer.recordaudio.GenerateAudioFileWithEffectsUseCase +import com.wire.android.ui.home.messagecomposer.recordaudio.RecordAudioFileGateway +import com.wire.android.ui.home.messagecomposer.recordaudio.RecordAudioViewModelFactory +import com.wire.android.ui.home.settings.about.dependencies.AndroidDependenciesInfoProvider +import com.wire.android.ui.home.settings.about.dependencies.DependenciesInfoProvider +import com.wire.android.ui.home.settings.about.dependencies.DependenciesViewModelFactory +import com.wire.android.ui.home.settings.about.licenses.AndroidLicensesProvider +import com.wire.android.ui.home.settings.about.licenses.LicensesProvider +import com.wire.android.ui.home.settings.about.licenses.LicensesViewModelFactory +import com.wire.android.ui.home.settings.appsettings.networkSettings.AndroidNetworkSettingsDefaultsProvider +import com.wire.android.ui.home.settings.appsettings.networkSettings.NetworkSettingsDefaultsProvider +import com.wire.android.ui.home.settings.appsettings.networkSettings.NetworkSettingsViewModelFactory +import com.wire.android.ui.home.settings.appearance.CustomizationViewModelFactory +import com.wire.android.ui.home.settings.SettingsViewModelFactory +import com.wire.android.ui.home.settings.account.MyAccountViewModelFactory +import com.wire.android.ui.home.settings.account.color.ChangeUserColorViewModelFactory +import com.wire.android.ui.home.settings.account.deleteAccount.DeleteAccountViewModelFactory +import com.wire.android.ui.home.settings.account.displayname.ChangeDisplayNameViewModelFactory +import com.wire.android.ui.home.settings.account.email.updateEmail.ChangeEmailViewModelFactory +import com.wire.android.ui.home.settings.account.email.verifyEmail.VerifyEmailViewModelFactory +import com.wire.android.ui.home.settings.account.handle.ChangeHandleViewModelFactory +import com.wire.android.ui.home.settings.backup.AndroidBackupFileGateway +import com.wire.android.ui.home.settings.backup.BackupAndRestoreViewModelFactory +import com.wire.android.ui.home.settings.backup.BackupFileGateway +import com.wire.android.ui.home.settings.backup.MPBackupSettings +import com.wire.android.ui.home.settings.privacy.PrivacySettingsViewModelFactory +import com.wire.android.ui.home.whatsnew.AndroidReleaseNotesFeedUrlProvider +import com.wire.android.ui.home.whatsnew.ReleaseNotesFeedUrlProvider +import com.wire.android.ui.home.whatsnew.WhatsNewViewModelFactory +import com.wire.android.ui.home.sync.FeatureFlagNotificationViewModelFactory +import com.wire.android.ui.analytics.AnalyticsConfiguration +import com.wire.android.ui.analytics.IsAnalyticsAvailableUseCase +import com.wire.android.ui.analytics.AnalyticsUsageViewModelFactory +import com.wire.android.feature.cells.ui.CellViewModelGraph +import com.wire.android.feature.cells.ui.CellViewModelFactory +import com.wire.android.feature.cells.ui.create.file.CreateFileViewModelFactory +import com.wire.android.feature.cells.ui.create.folder.CreateFolderViewModelFactory +import com.wire.android.feature.cells.ui.movetofolder.MoveToFolderViewModelFactory +import com.wire.android.feature.cells.ui.publiclink.PublicLinkViewModelFactory +import com.wire.android.feature.cells.ui.publiclink.settings.expiration.PublicLinkExpirationScreenViewModelFactory +import com.wire.android.feature.cells.ui.publiclink.settings.password.PublicLinkPasswordScreenViewModelFactory +import com.wire.android.feature.cells.ui.rename.RenameNodeViewModelFactory +import com.wire.android.feature.cells.ui.search.SearchScreenViewModelFactory +import com.wire.android.feature.cells.ui.tags.AddRemoveTagsViewModelFactory +import com.wire.android.feature.cells.ui.versioning.VersionHistoryViewModelFactory +import com.wire.android.feature.meetings.ui.MeetingViewModelGraph +import com.wire.android.feature.meetings.ui.list.MeetingListViewModelFactory +import com.wire.android.feature.meetings.ui.options.MeetingOptionsMenuViewModelFactory +import com.wire.android.ui.calling.common.SharedCallingViewModelFactory +import com.wire.android.ui.calling.incoming.IncomingCallViewModelFactory +import com.wire.android.ui.calling.ongoing.OngoingCallViewModelFactory +import com.wire.android.ui.calling.outgoing.OutgoingCallViewModelFactory +import com.wire.android.ui.calling.usecase.HangUpCallUseCase +import com.wire.android.ui.initialsync.InitialSyncViewModelFactory +import com.wire.android.ui.registration.code.CreateAccountVerificationCodeViewModelFactory +import com.wire.android.ui.registration.details.CreateAccountDataDetailViewModelFactory +import com.wire.android.ui.registration.selector.CreateAccountSelectorViewModelFactory +import com.wire.android.ui.settings.about.AboutThisAppInfoProvider +import com.wire.android.ui.settings.about.AboutThisAppViewModelFactory +import com.wire.android.ui.settings.about.AndroidAboutThisAppInfoProvider +import com.wire.android.ui.settings.devices.DeviceDetailsViewModelFactory +import com.wire.android.ui.settings.devices.SelfDevicesViewModelFactory +import com.wire.android.ui.settings.devices.e2ei.E2eiCertificateDetailsViewModelFactory +import com.wire.android.ui.home.conversations.media.CheckAssetRestrictionsViewModelFactory +import com.wire.android.ui.joinConversation.JoinConversationViaCodeViewModelFactory +import com.wire.android.ui.legalhold.dialog.deactivated.LegalHoldDeactivatedViewModelFactory +import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedViewModelFactory +import com.wire.android.ui.connection.ConnectionActionButtonViewModelFactory +import com.wire.android.ui.newauthentication.login.NewLoginRecoverableLogoutExceptionDetector +import com.wire.android.ui.newauthentication.login.NewLoginViewModelFactory +import com.wire.android.ui.newauthentication.login.ValidateEmailOrSSOCodeUseCase +import com.wire.android.ui.userprofile.avatarpicker.AndroidAvatarImageGateway +import com.wire.android.ui.userprofile.avatarpicker.AvatarImageGateway +import com.wire.android.ui.userprofile.avatarpicker.AvatarPickerViewModelFactory +import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModelFactory +import com.wire.android.ui.userprofile.qr.AndroidSelfQRCodeAssetRepository +import com.wire.android.ui.userprofile.qr.SelfQRCodeAssetRepository +import com.wire.android.ui.userprofile.qr.SelfQRCodeViewModelFactory +import com.wire.android.ui.userprofile.self.SelfUserProfileViewModelFactory +import com.wire.android.ui.userprofile.service.ServiceDetailsViewModelFactory +import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModelFactory +import com.wire.android.ui.sharing.ImportMediaAssetImporter +import com.wire.android.ui.sharing.ImportMediaAssetImporterImpl +import com.wire.android.ui.sharing.ImportMediaAuthenticatedViewModelFactory +import com.wire.android.util.AvatarImageManager +import com.wire.android.util.CurrentScreenManager +import com.wire.android.util.deeplink.DeepLinkProcessor +import com.wire.android.util.EMPTY +import com.wire.android.util.FileManager +import com.wire.android.util.GetMediaMetadataUseCase +import com.wire.android.util.GetMediaMetadataUseCaseImpl +import com.wire.android.util.ImageUtil +import com.wire.android.util.NetworkUtil +import com.wire.android.util.ScreenStateObserver +import com.wire.android.util.SwitchAccountObserver +import com.wire.android.util.UserAgentProvider +import com.wire.android.util.dispatchers.DefaultDispatcherProvider +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.android.util.isWebsocketEnabledByDefault +import com.wire.android.util.lifecycle.AutomatedLoginManager +import com.wire.android.util.lifecycle.IntentsProcessor +import com.wire.android.util.lifecycle.NomadIntentSignatureValidator +import com.wire.android.util.lifecycle.SyncLifecycleManager +import com.wire.android.util.logging.LogFileWriter +import com.wire.android.util.logging.LogFileWriterV1Impl +import com.wire.android.util.logging.LogFileWriterV2Impl +import com.wire.android.util.time.ISOFormatter +import com.wire.android.util.time.TimeZoneProvider +import com.wire.android.util.ui.AndroidUiTextResolver +import com.wire.android.util.ui.CountdownTimer +import com.wire.android.util.ui.UiTextResolver +import com.wire.android.util.ui.WireSessionImageLoader +import com.wire.android.workmanager.WireWorkerFactory +import com.wire.kalium.cells.CellsScope +import com.wire.kalium.cells.domain.CellUploadManager +import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCase +import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase +import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase +import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase +import com.wire.kalium.cells.domain.usecase.GetFoldersUseCase +import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase +import com.wire.kalium.cells.domain.usecase.GetMessageAttachmentUseCase +import com.wire.kalium.cells.domain.usecase.GetOwnersUseCase +import com.wire.kalium.cells.domain.usecase.GetPaginatedCellConversationsFlowUseCase +import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase +import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase +import com.wire.kalium.cells.domain.usecase.IsAtLeastOneCellAvailableUseCase +import com.wire.kalium.cells.domain.usecase.MoveNodeUseCase +import com.wire.kalium.cells.domain.usecase.ObserveAttachmentDraftsUseCase +import com.wire.kalium.cells.domain.usecase.RefreshCellAssetStateUseCase +import com.wire.kalium.cells.domain.usecase.RemoveAttachmentDraftUseCase +import com.wire.kalium.cells.domain.usecase.RemoveNodeTagsUseCase +import com.wire.kalium.cells.domain.usecase.RenameNodeUseCase +import com.wire.kalium.cells.domain.usecase.RestoreNodeFromRecycleBinUseCase +import com.wire.kalium.cells.domain.usecase.RetryAttachmentUploadUseCase +import com.wire.kalium.cells.domain.usecase.UpdateNodeTagsUseCase +import com.wire.kalium.cells.domain.usecase.create.CreateDocumentFileUseCase +import com.wire.kalium.cells.domain.usecase.create.CreateFolderUseCase +import com.wire.kalium.cells.domain.usecase.create.CreatePresentationFileUseCase +import com.wire.kalium.cells.domain.usecase.create.CreateSpreadsheetFileUseCase +import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase +import com.wire.kalium.cells.domain.usecase.download.DownloadCellVersionUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.CreatePublicLinkPasswordUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.CreatePublicLinkUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.DeletePublicLinkUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.GetPublicLinkPasswordUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.GetPublicLinkUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.SetPublicLinkExpirationUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.UpdatePublicLinkPasswordUseCase +import com.wire.kalium.cells.domain.usecase.versioning.GetNodeVersionsUseCase +import com.wire.kalium.cells.domain.usecase.versioning.RestoreNodeVersionUseCase +import com.wire.kalium.cells.paginatedConversationsFlowUseCase +import com.wire.kalium.cells.paginatedFilesFlowUseCase +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.data.conversation.FetchConversationUseCase +import com.wire.kalium.logic.data.conversation.ResetMLSConversationUseCase +import com.wire.kalium.logic.data.asset.KaliumFileSystem +import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase +import com.wire.kalium.logic.feature.appVersioning.ObserveIfAppUpdateRequiredUseCase +import com.wire.kalium.logic.feature.asset.GetAvatarAssetUseCase +import com.wire.kalium.logic.feature.asset.AudioNormalizedLoudnessBuilder +import com.wire.kalium.logic.feature.asset.DeleteAssetUseCase +import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase +import com.wire.kalium.logic.feature.asset.GetPaginatedFlowOfAssetMessageByConversationIdUseCase +import com.wire.kalium.logic.feature.asset.ObserveAssetStatusesUseCase +import com.wire.kalium.logic.feature.asset.ObservePaginatedAssetImageMessages +import com.wire.kalium.logic.feature.asset.UpdateAssetMessageTransferStatusUseCase +import com.wire.kalium.logic.feature.asset.upload.ScheduleNewAssetMessageUseCase +import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase +import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase +import com.wire.kalium.logic.feature.auth.sso.ValidateSSOCodeUseCase +import com.wire.kalium.logic.feature.auth.verification.RequestSecondFactorVerificationCodeUseCase +import com.wire.kalium.logic.feature.app.AppScope +import com.wire.kalium.logic.feature.app.GetAppByIdUseCase +import com.wire.kalium.logic.feature.app.ObserveAllAppsUseCase +import com.wire.kalium.logic.feature.app.ObserveIsAppMemberUseCase +import com.wire.kalium.logic.feature.app.SearchAppsByNameUseCase +import com.wire.kalium.logic.feature.backup.BackupScope +import com.wire.kalium.logic.feature.backup.CreateBackupUseCase +import com.wire.kalium.logic.feature.backup.CreateMPBackupUseCase +import com.wire.kalium.logic.feature.backup.CreateObfuscatedCopyUseCase +import com.wire.kalium.logic.feature.backup.RestoreBackupUseCase +import com.wire.kalium.logic.feature.backup.RestoreMPBackupUseCase +import com.wire.kalium.logic.feature.backup.VerifyBackupUseCase +import com.wire.kalium.logic.feature.call.CallsScope +import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase +import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase +import com.wire.kalium.logic.feature.call.usecase.FlipToBackCameraUseCase +import com.wire.kalium.logic.feature.call.usecase.FlipToFrontCameraUseCase +import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCase +import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCase +import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveCallModerationActionsUseCase +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.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 +import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase +import com.wire.kalium.logic.feature.call.usecase.SetCallQualityIntervalUseCase +import com.wire.kalium.logic.feature.call.usecase.SetUIRotationUseCase +import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase +import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase +import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase +import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase +import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase +import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase +import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase +import com.wire.kalium.logic.feature.channels.ChannelsScope +import com.wire.kalium.logic.feature.channels.ObserveChannelsCreationPermissionUseCase +import com.wire.kalium.logic.feature.client.ClientFingerprintUseCase +import com.wire.kalium.logic.feature.client.ClientScope +import com.wire.kalium.logic.feature.client.ClearNewClientsForUserUseCase +import com.wire.kalium.logic.feature.client.DeleteClientUseCase +import com.wire.kalium.logic.feature.client.FetchSelfClientsFromRemoteUseCase +import com.wire.kalium.logic.feature.client.FetchUsersClientsFromRemoteUseCase +import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment +import com.wire.kalium.logic.feature.client.GetOrRegisterClientUseCase +import com.wire.kalium.logic.feature.client.IsProfileQRCodeEnabledUseCase +import com.wire.kalium.logic.feature.client.IsWireCellsEnabledForConversationUseCase +import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase +import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase +import com.wire.kalium.logic.feature.client.ObserveClientDetailsUseCase +import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase +import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase +import com.wire.kalium.logic.feature.client.ObserveNewClientsUseCase +import com.wire.kalium.logic.feature.client.UpdateClientVerificationStatusUseCase +import com.wire.kalium.logic.feature.conversation.AddMemberToConversationUseCase +import com.wire.kalium.logic.feature.conversation.AddServiceToConversationUseCase +import com.wire.kalium.logic.feature.conversation.ObserveUsersTypingUseCase +import com.wire.kalium.logic.feature.connection.AcceptConnectionRequestUseCase +import com.wire.kalium.logic.feature.connection.BlockUserUseCase +import com.wire.kalium.logic.feature.connection.CancelConnectionRequestUseCase +import com.wire.kalium.logic.feature.connection.ConnectionScope +import com.wire.kalium.logic.feature.connection.IgnoreConnectionRequestUseCase +import com.wire.kalium.logic.feature.connection.SendConnectionRequestUseCase +import com.wire.kalium.logic.feature.connection.UnblockUserUseCase +import com.wire.kalium.logic.feature.analytics.GetCurrentAnalyticsTrackingIdentifierUseCase +import com.wire.kalium.logic.feature.conversation.CheckConversationLeaveConditionsUseCase +import com.wire.kalium.logic.feature.conversation.ClearUsersTypingEventsUseCase +import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase +import com.wire.kalium.logic.feature.conversation.ConversationScope +import com.wire.kalium.logic.feature.conversation.GetConversationUnreadEventsCountUseCase +import com.wire.kalium.logic.feature.conversation.GetOrCreateOneToOneConversationUseCase +import com.wire.kalium.logic.feature.conversation.GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase +import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedUseCase +import com.wire.kalium.logic.feature.conversation.JoinConversationViaCodeUseCase +import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase +import com.wire.kalium.logic.feature.conversation.ObserveArchivedUnreadConversationsCountUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConversationAdminRoleUseCase +import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase +import com.wire.kalium.logic.feature.conversation.NotifyConversationIsOpenUseCase +import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationUnderLegalHoldNotifiedUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsWithEventsUseCase +import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase +import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase +import com.wire.kalium.logic.feature.conversation.PromoteAdminAndLeaveConversationUseCase +import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase +import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase +import com.wire.kalium.logic.feature.conversation.RenameConversationUseCase +import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase +import com.wire.kalium.logic.feature.conversation.SetNotifiedAboutConversationUnderLegalHoldUseCase +import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase +import com.wire.kalium.logic.feature.conversation.SyncConversationCodeUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationReceiptModeUseCase +import com.wire.kalium.logic.feature.conversation.apps.ChangeAccessForAppsInConversationUseCase +import com.wire.kalium.logic.feature.conversation.channel.UpdateChannelAddPermissionUseCase +import com.wire.kalium.logic.feature.conversation.createconversation.CreateChannelUseCase +import com.wire.kalium.logic.feature.conversation.createconversation.CreateRegularGroupUseCase +import com.wire.kalium.logic.feature.conversation.delete.MarkConversationAsDeletedLocallyUseCase +import com.wire.kalium.logic.feature.conversation.messagetimer.UpdateMessageTimerUseCase +import com.wire.kalium.logic.feature.conversation.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery +import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase +import com.wire.kalium.logic.feature.conversation.folder.CreateConversationFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.MoveConversationToFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFolderUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.CanCreatePasswordProtectedLinksUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.ObserveGuestRoomLinkUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.RevokeGuestRoomLinkUseCase +import com.wire.kalium.logic.feature.debug.BreakSessionUseCase +import com.wire.kalium.logic.feature.debug.ChangeProfilingUseCase +import com.wire.kalium.logic.feature.debug.DebugScope +import com.wire.kalium.logic.feature.debug.DebugFeedConversationUseCase +import com.wire.kalium.logic.feature.debug.GetDebugE2EICertificateExpirationUseCase +import com.wire.kalium.logic.feature.debug.GetConversationCryptoStatsUseCase +import com.wire.kalium.logic.feature.debug.GetConversationEpochFromCCUseCase +import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase +import com.wire.kalium.logic.feature.debug.ObserveIsConsumableNotificationsEnabledUseCase +import com.wire.kalium.logic.feature.debug.ObserveDatabaseLoggerStateUseCase +import com.wire.kalium.logic.feature.debug.RepairFaultyRemovalKeysUseCase +import com.wire.kalium.logic.feature.debug.SetDebugE2EICertificateExpirationUseCase +import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsUseCase +import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.FetchConversationMLSVerificationStatusUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.GetMLSClientIdentityUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.GetMembersE2EICertificateStatusesUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.GetUserMlsClientIdentitiesUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase +import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase +import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase +import com.wire.kalium.logic.feature.incallreaction.SendInCallReactionUseCase +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import com.wire.kalium.logic.feature.session.DeleteSessionUseCase +import com.wire.kalium.logic.feature.session.DoesValidNomadAccountExistUseCase +import com.wire.kalium.logic.feature.session.DoesValidSessionExistUseCase +import com.wire.kalium.logic.feature.session.GetSessionsUseCase +import com.wire.kalium.logic.feature.session.ObserveSessionsUseCase +import com.wire.kalium.logic.feature.session.UpdateCurrentSessionUseCase +import com.wire.kalium.logic.feature.team.TeamScope +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase +import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamUseCase +import com.wire.kalium.logic.feature.server.GetServerConfigUseCase +import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase +import com.wire.kalium.logic.feature.service.GetServiceByIdUseCase +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 com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase +import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase +import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase +import com.wire.kalium.logic.feature.message.DeleteMessageUseCase +import com.wire.kalium.logic.feature.message.FetchOlderNomadMessagesByConversationUseCase +import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase +import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesByConversationUseCase +import com.wire.kalium.logic.feature.message.GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase +import com.wire.kalium.logic.feature.message.GetSearchedConversationMessagePositionUseCase +import com.wire.kalium.logic.feature.message.MessageScope +import com.wire.kalium.logic.feature.message.ObserveMessageByIdUseCase +import com.wire.kalium.logic.feature.message.ObserveMessageReactionsUseCase +import com.wire.kalium.logic.feature.message.ObserveMessageReceiptsUseCase +import com.wire.kalium.logic.feature.message.RetryFailedMessageUseCase +import com.wire.kalium.logic.feature.message.SendEditMultipartMessageUseCase +import com.wire.kalium.logic.feature.message.SendEditTextMessageUseCase +import com.wire.kalium.logic.feature.message.SendKnockUseCase +import com.wire.kalium.logic.feature.message.SendLocationUseCase +import com.wire.kalium.logic.feature.message.SendMultipartMessageUseCase +import com.wire.kalium.logic.feature.message.SendTextMessageUseCase +import com.wire.kalium.logic.feature.message.ToggleReactionUseCase +import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase +import com.wire.kalium.logic.feature.message.draft.GetMessageDraftUseCase +import com.wire.kalium.logic.feature.message.draft.RemoveMessageDraftUseCase +import com.wire.kalium.logic.feature.message.draft.SaveMessageDraftUseCase +import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase +import com.wire.kalium.logic.feature.message.fetchOlderMessagesByConversationId +import com.wire.kalium.logic.feature.message.getPaginatedFlowOfAssetMessageByConversationId +import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesByConversation +import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesBySearchQueryAndConversation +import com.wire.kalium.logic.feature.message.observePaginatedImageAssetMessageByConversationId +import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase +import com.wire.kalium.logic.feature.search.FederatedSearchParser +import com.wire.kalium.logic.feature.search.IsFederationSearchAllowedUseCase +import com.wire.kalium.logic.feature.search.SearchByHandleUseCase +import com.wire.kalium.logic.feature.search.SearchScope +import com.wire.kalium.logic.feature.search.SearchUsersUseCase +import com.wire.kalium.logic.feature.sessionreset.ResetSessionUseCase +import com.wire.kalium.logic.feature.team.SyncSelfTeamInfoUseCase +import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase +import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase +import com.wire.kalium.logic.feature.notificationToken.SendFCMTokenUseCase +import com.wire.kalium.logic.feature.user.DeleteAccountUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase +import com.wire.kalium.logic.feature.user.IsMLSEnabledUseCase +import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase +import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase +import com.wire.kalium.logic.feature.user.IsSelfATeamMemberUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase +import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase +import com.wire.kalium.logic.feature.user.ObserveValidAccountsUseCase +import com.wire.kalium.logic.feature.user.SetUserHandleUseCase +import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase +import com.wire.kalium.logic.feature.user.UpdateAccentColorUseCase +import com.wire.kalium.logic.feature.user.UpdateDisplayNameUseCase +import com.wire.kalium.logic.feature.user.UpdateEmailUseCase +import com.wire.kalium.logic.feature.user.UpdateSelfAvailabilityStatusUseCase +import com.wire.kalium.logic.feature.user.UploadUserAvatarUseCase +import com.wire.kalium.logic.feature.user.UserScope +import com.wire.kalium.logic.feature.user.readReceipts.ObserveReadReceiptsEnabledUseCase +import com.wire.kalium.logic.feature.user.readReceipts.PersistReadReceiptsStatusConfigUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCase +import com.wire.kalium.logic.feature.user.typingIndicator.ObserveTypingIndicatorEnabledUseCase +import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicatorStatusConfigUseCase +import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase +import com.wire.kalium.logic.feature.user.webSocketStatus.PersistPersistentWebSocketConnectionStatusUseCase +import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCase +import com.wire.kalium.logic.featureFlags.BuildFileRestrictionState +import com.wire.kalium.logic.featureFlags.KaliumConfigs +import com.wire.kalium.logic.sync.ForegroundActionsUseCase +import com.wire.kalium.logic.sync.ObserveSyncStateUseCase +import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler +import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase +import com.wire.kalium.logic.util.RandomPassword +import com.wire.kalium.network.NetworkStateObserver +import com.wire.kalium.util.DelicateKaliumApi +import com.wire.android.di.ApplicationContext +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Named +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.createGraphFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.runBlocking + +abstract class WireMetroScope private constructor() + +@DependencyGraph(WireMetroScope::class) +@Suppress("LargeClass", "TooManyFunctions") +interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAssetViewModelGraph { + @DependencyGraph.Factory + fun interface Factory { + fun create(@Provides @ApplicationContext context: Context): WireMetroGraph + } + + val checkAssetRestrictionsViewModelFactory: CheckAssetRestrictionsViewModelFactory + val aboutThisAppViewModelFactory: AboutThisAppViewModelFactory + val createAccountCodeViewModelFactory: CreateAccountCodeViewModelFactory + val createAccountDetailsViewModelFactory: CreateAccountDetailsViewModelFactory + val createAccountDataDetailViewModelFactory: CreateAccountDataDetailViewModelFactory + val createAccountEmailViewModelFactory: CreateAccountEmailViewModelFactory + val createAccountOverviewViewModelFactory: CreateAccountOverviewViewModelFactory + val createAccountSummaryViewModelFactory: CreateAccountSummaryViewModelFactory + val createAccountUsernameViewModelFactory: CreateAccountUsernameViewModelFactory + val createAccountVerificationCodeViewModelFactory: CreateAccountVerificationCodeViewModelFactory + val createAccountSelectorViewModelFactory: CreateAccountSelectorViewModelFactory + val loginEmailViewModelFactory: LoginEmailViewModelFactory + val loginSSOViewModelFactory: LoginSSOViewModelFactory + val whatsNewViewModelFactory: WhatsNewViewModelFactory + val dependenciesViewModelFactory: DependenciesViewModelFactory + val licensesViewModelFactory: LicensesViewModelFactory + val userDebugViewModelFactory: UserDebugViewModelFactory + val conversationCryptoStatsViewModelFactory: ConversationCryptoStatsViewModelFactory + val debugConversationViewModelFactory: DebugConversationViewModelFactory + val logManagementViewModelFactory: LogManagementViewModelFactory + val debugFeatureFlagsViewModelFactory: DebugFeatureFlagsViewModelFactory + val debugDataOptionsViewModelFactory: DebugDataOptionsViewModelFactory + val exportObfuscatedCopyViewModelFactory: ExportObfuscatedCopyViewModelFactory + val customizationViewModelFactory: CustomizationViewModelFactory + val initialSyncViewModelFactory: InitialSyncViewModelFactory + val networkSettingsViewModelFactory: NetworkSettingsViewModelFactory + val e2EIEnrollmentViewModelFactory: E2EIEnrollmentViewModelFactory + val getE2EICertificateViewModelFactory: GetE2EICertificateViewModelFactory + val clearSessionViewModelFactory: ClearSessionViewModelFactory + val removeDeviceViewModelFactory: RemoveDeviceViewModelFactory + val registerDeviceViewModelFactory: RegisterDeviceViewModelFactory + val settingsViewModelFactory: SettingsViewModelFactory + val selfDevicesViewModelFactory: SelfDevicesViewModelFactory + val deviceDetailsViewModelFactory: DeviceDetailsViewModelFactory + val e2eiCertificateDetailsViewModelFactory: E2eiCertificateDetailsViewModelFactory + val avatarPickerViewModelFactory: AvatarPickerViewModelFactory + val changeUserColorViewModelFactory: ChangeUserColorViewModelFactory + val changeEmailViewModelFactory: ChangeEmailViewModelFactory + val verifyEmailViewModelFactory: VerifyEmailViewModelFactory + val changeDisplayNameViewModelFactory: ChangeDisplayNameViewModelFactory + val changeHandleViewModelFactory: ChangeHandleViewModelFactory + val myAccountViewModelFactory: MyAccountViewModelFactory + val deleteAccountViewModelFactory: DeleteAccountViewModelFactory + val backupAndRestoreViewModelFactory: BackupAndRestoreViewModelFactory + val appUnlockWithBiometricsViewModelFactory: AppUnlockWithBiometricsViewModelFactory + val enterLockScreenViewModelFactory: EnterLockScreenViewModelFactory + val newFolderViewModelFactory: NewFolderViewModelFactory + val forgotLockScreenViewModelFactory: ForgotLockScreenViewModelFactory + val setLockScreenViewModelFactory: SetLockScreenViewModelFactory + val privacySettingsViewModelFactory: PrivacySettingsViewModelFactory + val selfQRCodeViewModelFactory: SelfQRCodeViewModelFactory + val mediaGalleryViewModelFactory: MediaGalleryViewModelFactory + val imagesPreviewViewModelFactory: ImagesPreviewViewModelFactory + val otherUserProfileScreenViewModelFactory: OtherUserProfileScreenViewModelFactory + val selfUserProfileViewModelFactory: SelfUserProfileViewModelFactory + val serviceDetailsViewModelFactory: ServiceDetailsViewModelFactory + val welcomeViewModelFactory: WelcomeViewModelFactory + val newLoginViewModelFactory: NewLoginViewModelFactory + val importMediaAuthenticatedViewModelFactory: ImportMediaAuthenticatedViewModelFactory + val joinConversationViaCodeViewModelFactory: JoinConversationViaCodeViewModelFactory + val securityClassificationViewModelFactory: SecurityClassificationViewModelFactory + val connectionActionButtonViewModelFactory: ConnectionActionButtonViewModelFactory + val conversationOptionsMenuViewModelFactory: ConversationOptionsMenuViewModelFactory + val isFileSharingEnabledViewModelFactory: IsFileSharingEnabledViewModelFactory + val addMembersToConversationViewModelFactory: AddMembersToConversationViewModelFactory + val promoteAdminViewModelFactory: PromoteAdminViewModelFactory + val editConversationMetadataViewModelFactory: EditConversationMetadataViewModelFactory + val searchAppsViewModelFactory: SearchAppsViewModelFactory + val recordAudioViewModelFactory: RecordAudioViewModelFactory + val updateAppsAccessViewModelFactory: UpdateAppsAccessViewModelFactory + val updateChannelAccessViewModelFactory: UpdateChannelAccessViewModelFactory + val editSelfDeletingMessagesViewModelFactory: EditSelfDeletingMessagesViewModelFactory + val editGuestAccessViewModelFactory: EditGuestAccessViewModelFactory + val createPasswordGuestLinkViewModelFactory: CreatePasswordGuestLinkViewModelFactory + val searchConversationMessagesViewModelFactory: SearchConversationMessagesViewModelFactory + val groupConversationParticipantsViewModelFactory: GroupConversationParticipantsViewModelFactory + val messageDetailsViewModelFactory: MessageDetailsViewModelFactory + val searchUserViewModelFactory: SearchUserViewModelFactory + val conversationFoldersViewModelFactory: ConversationFoldersViewModelFactory + val moveConversationToFolderViewModelFactory: MoveConversationToFolderViewModelFactory + val typingIndicatorViewModelFactory: TypingIndicatorViewModelFactory + val locationPickerViewModelFactory: LocationPickerViewModelFactory + val selfDeletingMessageActionViewModelFactory: SelfDeletingMessageActionViewModelFactory + val conversationAssetPathsViewModelFactory: ConversationAssetPathsViewModelFactory + val multipartAttachmentsViewModelFactory: MultipartAttachmentsViewModelFactory + val quotedMultipartMessageViewModelFactory: QuotedMultipartMessageViewModelFactory + val messageOptionsMenuViewModelFactory: MessageOptionsMenuViewModelFactory + val groupConversationDetailsViewModelFactory: GroupConversationDetailsViewModelFactory + val compositeMessageViewModelFactory: CompositeMessageViewModelFactory + val assetLocalPathViewModelFactory: AssetLocalPathViewModelFactory + val conversationAssetMessagesViewModelFactory: ConversationAssetMessagesViewModelFactory + val conversationMessagesViewModelFactory: ConversationMessagesViewModelFactory + val audioMessageViewModelFactory: AudioMessageViewModelFactory + val conversationInfoViewModelFactory: ConversationInfoViewModelFactory + val conversationBannerViewModelFactory: ConversationBannerViewModelFactory + val conversationCallViewModelFactory: ConversationCallViewModelFactory + val messageComposerViewModelFactory: MessageComposerViewModelFactory + val sendMessageViewModelFactory: SendMessageViewModelFactory + val conversationMigrationViewModelFactory: ConversationMigrationViewModelFactory + val messageDraftViewModelFactory: MessageDraftViewModelFactory + val messageAttachmentsViewModelFactory: MessageAttachmentsViewModelFactory + val homeViewModelFactory: HomeViewModelFactory + val appSyncViewModelFactory: AppSyncViewModelFactory + val homeDrawerViewModelFactory: HomeDrawerViewModelFactory + val newConversationViewModelFactory: NewConversationViewModelFactory + val analyticsUsageViewModelFactory: AnalyticsUsageViewModelFactory + override val cellViewModelFactory: CellViewModelFactory + override val createFileViewModelFactory: CreateFileViewModelFactory + override val createFolderViewModelFactory: CreateFolderViewModelFactory + override val renameNodeViewModelFactory: RenameNodeViewModelFactory + override val moveToFolderViewModelFactory: MoveToFolderViewModelFactory + override val addRemoveTagsViewModelFactory: AddRemoveTagsViewModelFactory + override val versionHistoryViewModelFactory: VersionHistoryViewModelFactory + override val publicLinkViewModelFactory: PublicLinkViewModelFactory + override val publicLinkExpirationScreenViewModelFactory: PublicLinkExpirationScreenViewModelFactory + override val publicLinkPasswordScreenViewModelFactory: PublicLinkPasswordScreenViewModelFactory + override val searchScreenViewModelFactory: SearchScreenViewModelFactory + val teamMigrationViewModelFactory: TeamMigrationViewModelFactory + val incomingCallViewModelFactory: IncomingCallViewModelFactory + val outgoingCallViewModelFactory: OutgoingCallViewModelFactory + val ongoingCallViewModelFactory: OngoingCallViewModelFactory + val sharedCallingViewModelFactory: SharedCallingViewModelFactory + val conversationListViewModelFactory: ConversationListViewModelFactory + val conversationListCallViewModelFactory: ConversationListCallViewModelFactory + val commonTopAppBarViewModelFactory: CommonTopAppBarViewModelFactory + val featureFlagNotificationViewModelFactory: FeatureFlagNotificationViewModelFactory + val legalHoldRequestedViewModelFactory: LegalHoldRequestedViewModelFactory + val legalHoldDeactivatedViewModelFactory: LegalHoldDeactivatedViewModelFactory + val wireActivityViewModelFactory: WireActivityViewModelFactory + val callFeedbackViewModelFactory: CallFeedbackViewModelFactory + val callActivityViewModelFactory: CallActivityViewModelFactory + override val meetingListViewModelFactory: MeetingListViewModelFactory + override val meetingOptionsMenuViewModelFactory: MeetingOptionsMenuViewModelFactory + override val imageAssetViewModelFactory: ImageAssetViewModelFactory + + val currentScreenManager: CurrentScreenManager + val lockCodeTimeManager: LockCodeTimeManager + val switchAccountObserver: SwitchAccountObserver + val loginTypeSelector: LoginTypeSelector + val dynamicReceiversManager: DynamicReceiversManager + val managedConfigurationsManager: ManagedConfigurationsManager + val syncLifecycleManager: SyncLifecycleManager + val proximitySensorManager: ProximitySensorManager + val servicesManager: ServicesManager + val callNotificationManager: CallNotificationManager + val conversationAudioMessagePlayer: ConversationAudioMessagePlayer + val logFileWriter: LogFileWriter + val wireWorkerFactory: WireWorkerFactory + val globalObserversManager: GlobalObserversManager + val globalDataStore: GlobalDataStore + val userDataStoreProvider: UserDataStoreProvider + val analyticsManager: AnonymousAnalyticsManager + val workManager: WorkManager + + val dispatcherProvider: DispatcherProvider + + @get:ApplicationScope + val applicationScope: CoroutineScope + + @get:KaliumCoreLogic + val coreLogic: CoreLogic + + val networkUtil: NetworkUtil + val persistentWebSocketServiceDependencies: PersistentWebSocketService.Dependencies + val callServiceDependencies: CallService.Dependencies + val playingAudioMessageServiceDependencies: PlayingAudioMessageService.Dependencies + val currentSession: CurrentSessionUseCase + val accountSwitch: AccountSwitchUseCase + val nomadProfilesFeatureConfig: NomadProfilesFeatureConfig + val startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase + + @get:CurrentAccount + val currentAccount: UserId + + @Provides + @SingleIn(WireMetroScope::class) + fun provideGlobalDataStore( + @ApplicationContext context: Context, + ): GlobalDataStore = GlobalDataStore(context) + + @Provides + @SingleIn(WireMetroScope::class) + fun provideUserAgentProvider( + @ApplicationContext context: Context, + ): UserAgentProvider = + UserAgentProvider(context) + + @Provides + @KaliumCoreLogic + @SingleIn(WireMetroScope::class) + fun provideKaliumCoreLogic( + @ApplicationContext context: Context, + kaliumConfigs: KaliumConfigs, + userAgentProvider: UserAgentProvider, + ): CoreLogic { + val rootPath = context.getDir("accounts", Context.MODE_PRIVATE).path + return CoreLogic( + userAgent = userAgentProvider.defaultUserAgent, + appContext = context, + rootPath = rootPath, + kaliumConfigs = kaliumConfigs, + ) + } + + @Provides + @KaliumCoreLogic + fun provideKaliumCoreLogicLazy(@KaliumCoreLogic coreLogic: CoreLogic): dagger.Lazy = + object : dagger.Lazy { + override fun get(): CoreLogic = coreLogic + } + + @Provides + @SingleIn(WireMetroScope::class) + fun provideUserDataStoreProvider( + @ApplicationContext context: Context, + ): UserDataStoreProvider = + UserDataStoreProvider(context) + + @Provides + fun provideCurrentAccountUserDataStore( + @CurrentAccount currentAccount: UserId, + userDataStoreProvider: UserDataStoreProvider, + ): UserDataStore = + userDataStoreProvider.getOrCreate(currentAccount) + + @Provides + fun provideCurrentAccountUserDataStoreLazy(userDataStore: UserDataStore): dagger.Lazy = + object : dagger.Lazy { + override fun get(): UserDataStore = userDataStore + } + + @Provides + fun provideGlobalDataStoreLazy(globalDataStore: GlobalDataStore): dagger.Lazy = + object : dagger.Lazy { + override fun get(): GlobalDataStore = globalDataStore + } + + @Provides + fun provideDispatchers(): DispatcherProvider = DefaultDispatcherProvider() + + @Provides + fun provideAutomatedLoginManager(): AutomatedLoginManager = + AutomatedLoginManager() + + @Provides + fun provideServerConfigProvider(): ServerConfigProvider = + ServerConfigProvider() + + @Provides + fun provideAndroidUserContextProvider(): AndroidUserContextProvider = + AndroidUserContextProviderImpl() + + @Provides + fun provideManagedConfigParser( + userContextProvider: AndroidUserContextProvider, + ): ManagedConfigParser = + ManagedConfigParserImpl(userContextProvider) + + @Provides + @SingleIn(WireMetroScope::class) + fun provideManagedConfigurationsManager( + @ApplicationContext context: Context, + dispatcherProvider: DispatcherProvider, + serverConfigProvider: ServerConfigProvider, + globalDataStore: GlobalDataStore, + configParser: ManagedConfigParser, + ): ManagedConfigurationsManager = + ManagedConfigurationsManagerImpl( + context = context, + dispatchers = dispatcherProvider, + serverConfigProvider = serverConfigProvider, + globalDataStore = globalDataStore, + configParser = configParser, + ) + + @Provides + fun provideCurrentServerConfig( + managedConfigurationsManager: ManagedConfigurationsManager, + serverConfigProvider: ServerConfigProvider, + ): ServerConfig.Links = + if (BuildConfig.EMM_SUPPORT_ENABLED) { + managedConfigurationsManager.currentServerConfig + } else { + serverConfigProvider.getDefaultServerConfig(null) + } + + @Named("ssoCodeConfig") + @Provides + fun provideCurrentSSOCodeConfig( + managedConfigurationsManager: ManagedConfigurationsManager, + ): String = + if (BuildConfig.EMM_SUPPORT_ENABLED) { + managedConfigurationsManager.currentSSOCodeConfig + } else { + String.EMPTY + } + + @Provides + fun provideLoginSavedInputStore(): LoginSavedInputStore = + SavedStateLoginSavedInputStore(SavedStateHandle()) + + @Provides + fun provideCountdownTimer(): CountdownTimer = + CountdownTimer() + + @Provides + fun provideClientScopeProviderFactory( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ClientScopeProvider.Factory = + object : ClientScopeProvider.Factory { + override fun create(userId: UserId): ClientScopeProvider = + ClientScopeProvider(coreLogic, userId) + } + + @Provides + fun provideAddAuthenticatedUserUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + ): AddAuthenticatedUserUseCase = + coreLogic.getGlobalScope().addAuthenticatedAccount + + @DefaultWebSocketEnabledByDefault + @Provides + fun provideDefaultWebSocketEnabledByDefault( + @ApplicationContext context: Context, + managedConfigurationsManager: ManagedConfigurationsManager, + ): Boolean = + isWebsocketEnabledByDefault( + context, + managedConfigurationsManager.persistentWebSocketEnforcedByMDM.value + ) + + @Provides + fun provideLoginSSOSessionExceptionClassifier(): LoginSSOSessionExceptionClassifier = + LoginSSOSessionExceptionClassifier() + + @Provides + fun provideCurrentTimestampProvider(): CurrentTimestampProvider = + CurrentTimestampProvider { System.currentTimeMillis() } + + @Provides + fun provideGeocoder(@ApplicationContext context: Context): Geocoder = + Geocoder(context) + + @Provides + fun provideLocationPickerParameters(): LocationPickerParameters = + LocationPickerParameters() + + @Provides + @SingleIn(WireMetroScope::class) + fun provideLockCodeTimeManager( + @ApplicationScope appCoroutineScope: CoroutineScope, + currentScreenManager: CurrentScreenManager, + observeAppLockConfigUseCase: ObserveAppLockConfigUseCase, + globalDataStore: GlobalDataStore, + currentTimestamp: CurrentTimestampProvider, + ): LockCodeTimeManager = + LockCodeTimeManager( + appCoroutineScope = appCoroutineScope, + currentScreenManager = currentScreenManager, + observeAppLockConfigUseCase = observeAppLockConfigUseCase, + globalDataStore = globalDataStore, + currentTimestamp = currentTimestamp, + ) + + @Provides + fun provideNotificationManagerCompat(@ApplicationContext context: Context): NotificationManagerCompat = + NotificationManagerCompat.from(context) + + @Provides + fun provideNotificationManager(@ApplicationContext context: Context): NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + @Provides + @SingleIn(WireMetroScope::class) + fun provideMessageNotificationManager( + @ApplicationContext context: Context, + notificationManagerCompat: NotificationManagerCompat, + notificationManager: NotificationManager, + lockCodeTimeManager: LockCodeTimeManager, + ): MessageNotificationManager = + MessageNotificationManager( + context = context, + notificationManagerCompat = notificationManagerCompat, + notificationManager = notificationManager, + lockCodeTimeManager = lockCodeTimeManager, + ) + + @Provides + @SingleIn(WireMetroScope::class) + fun provideCallNotificationBuilder(@ApplicationContext context: Context): CallNotificationBuilder = + CallNotificationBuilder(context) + + @Provides + @SingleIn(WireMetroScope::class) + fun provideCallNotificationManager( + @ApplicationContext context: Context, + dispatcherProvider: DispatcherProvider, + builder: CallNotificationBuilder, + @KaliumCoreLogic coreLogic: CoreLogic, + ): CallNotificationManager = + CallNotificationManager( + context = context, + dispatcherProvider = dispatcherProvider, + builder = builder, + coreLogic = coreLogic, + qualifiedIdMapper = QualifiedIdMapper(null), + ) + + @Provides + @SingleIn(WireMetroScope::class) + @Suppress("LongParameterList") + fun provideWireNotificationManager( + @KaliumCoreLogic coreLogic: CoreLogic, + currentScreenManager: CurrentScreenManager, + messagesNotificationManager: MessageNotificationManager, + callNotificationManager: CallNotificationManager, + syncLifecycleManager: SyncLifecycleManager, + servicesManager: ServicesManager, + dispatcherProvider: DispatcherProvider, + pingRinger: PingRinger, + ): WireNotificationManager = + WireNotificationManager( + coreLogic = coreLogic, + currentScreenManager = currentScreenManager, + messagesNotificationManager = messagesNotificationManager, + callNotificationManager = callNotificationManager, + syncLifecycleManager = syncLifecycleManager, + servicesManager = servicesManager, + dispatcherProvider = dispatcherProvider, + pingRinger = pingRinger, + ) + + @Provides + @SingleIn(WireMetroScope::class) + fun provideSyncLifecycleManager( + currentScreenManager: CurrentScreenManager, + @KaliumCoreLogic coreLogic: CoreLogic, + ): SyncLifecycleManager = + SyncLifecycleManager( + currentScreenManager = currentScreenManager, + coreLogic = coreLogic, + ) + + @Provides + @SingleIn(WireMetroScope::class) + fun provideLogFileWriter(@ApplicationContext context: Context): LogFileWriter { + val logsDirectory = LogFileWriter.logsDirectory(context) + return if (BuildConfig.USE_ASYNC_FLUSH_LOGGING) { + LogFileWriterV2Impl(logsDirectory) + } else { + LogFileWriterV1Impl(logsDirectory) + } + } + + @Provides + @SingleIn(WireMetroScope::class) + fun provideAnonymousAnalyticsManager(): AnonymousAnalyticsManager = + AnonymousAnalyticsManagerImpl + + @Provides + fun provideAnonymousAnalyticsManagerLazy( + anonymousAnalyticsManager: AnonymousAnalyticsManager, + ): dagger.Lazy = + object : dagger.Lazy { + override fun get(): AnonymousAnalyticsManager = anonymousAnalyticsManager + } + + @Provides + fun provideLoginTypeSelector( + @KaliumCoreLogic coreLogic: dagger.Lazy, + ): LoginTypeSelector = + LoginTypeSelector( + coreLogic = coreLogic, + useNewLoginForDefaultBackend = BuildConfig.USE_NEW_LOGIN_FOR_DEFAULT_BACKEND, + ) + + @Provides + fun provideCurrentSessionFlowUseCase(@KaliumCoreLogic coreLogic: CoreLogic): CurrentSessionFlowUseCase = + coreLogic.getGlobalScope().session.currentSessionFlow + + @Provides + fun provideCurrentSessionFlowUseCaseLazy(currentSessionFlow: CurrentSessionFlowUseCase): dagger.Lazy = + object : dagger.Lazy { + override fun get(): CurrentSessionFlowUseCase = currentSessionFlow + } + + @Provides + fun provideDoesValidSessionExistUseCase(@KaliumCoreLogic coreLogic: CoreLogic): DoesValidSessionExistUseCase = + coreLogic.getGlobalScope().doesValidSessionExist + + @Provides + fun provideDoesValidSessionExistUseCaseLazy( + doesValidSessionExist: DoesValidSessionExistUseCase, + ): dagger.Lazy = + object : dagger.Lazy { + override fun get(): DoesValidSessionExistUseCase = doesValidSessionExist + } + + @Provides + fun provideGetServerConfigUseCase(@KaliumCoreLogic coreLogic: CoreLogic): GetServerConfigUseCase = + coreLogic.getGlobalScope().fetchServerConfigFromDeepLink + + @Provides + fun provideGetServerConfigUseCaseLazy(getServerConfig: GetServerConfigUseCase): dagger.Lazy = + object : dagger.Lazy { + override fun get(): GetServerConfigUseCase = getServerConfig + } + + @Provides + fun provideObserveSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ObserveSessionsUseCase = + coreLogic.getGlobalScope().session.allSessionsFlow + + @Provides + fun provideObserveSessionsUseCaseLazy(observeSessions: ObserveSessionsUseCase): dagger.Lazy = + object : dagger.Lazy { + override fun get(): ObserveSessionsUseCase = observeSessions + } + + @Provides + fun provideObserveIfAppUpdateRequiredUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ObserveIfAppUpdateRequiredUseCase = + coreLogic.getGlobalScope().observeIfAppUpdateRequired + + @Provides + fun provideObserveIfAppUpdateRequiredUseCaseLazy( + observeIfAppUpdateRequired: ObserveIfAppUpdateRequiredUseCase, + ): dagger.Lazy = + object : dagger.Lazy { + override fun get(): ObserveIfAppUpdateRequiredUseCase = observeIfAppUpdateRequired + } + + @Provides + fun provideObserveNewClientsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ObserveNewClientsUseCase = + coreLogic.getGlobalScope().observeNewClientsUseCase + + @Provides + fun provideObserveNewClientsUseCaseLazy(observeNewClients: ObserveNewClientsUseCase): dagger.Lazy = + object : dagger.Lazy { + override fun get(): ObserveNewClientsUseCase = observeNewClients + } + + @Provides + fun provideClearNewClientsForUserUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ClearNewClientsForUserUseCase = + coreLogic.getGlobalScope().clearNewClientsForUser + + @Provides + fun provideClearNewClientsForUserUseCaseLazy( + clearNewClientsForUser: ClearNewClientsForUserUseCase, + ): dagger.Lazy = + object : dagger.Lazy { + override fun get(): ClearNewClientsForUserUseCase = clearNewClientsForUser + } + + @Provides + fun provideDoesValidNomadAccountExistUseCaseLazy( + doesValidNomadAccountExist: DoesValidNomadAccountExistUseCase, + ): dagger.Lazy = + object : dagger.Lazy { + override fun get(): DoesValidNomadAccountExistUseCase = doesValidNomadAccountExist + } + + @Provides + fun provideAccountSwitchUseCaseLazy(accountSwitch: AccountSwitchUseCase): dagger.Lazy = + object : dagger.Lazy { + override fun get(): AccountSwitchUseCase = accountSwitch + } + + @Provides + fun provideServicesManagerLazy(servicesManager: ServicesManager): dagger.Lazy = + object : dagger.Lazy { + override fun get(): ServicesManager = servicesManager + } + + @Provides + @SingleIn(WireMetroScope::class) + fun provideServicesManager( + @ApplicationContext context: Context, + dispatcherProvider: DispatcherProvider, + ): ServicesManager = + ServicesManager( + context = context, + dispatcherProvider = dispatcherProvider, + ) + + @Provides + fun provideWorkManagerLazy(workManager: WorkManager): dagger.Lazy = + object : dagger.Lazy { + override fun get(): WorkManager = workManager + } + + @Provides + fun provideCurrentScreenManagerLazy(currentScreenManager: CurrentScreenManager): dagger.Lazy = + object : dagger.Lazy { + override fun get(): CurrentScreenManager = currentScreenManager + } + + @Provides + fun provideObserveSyncStateUseCaseProviderFactory( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ObserveSyncStateUseCaseProvider.Factory = + object : ObserveSyncStateUseCaseProvider.Factory { + override fun create(userId: UserId): ObserveSyncStateUseCaseProvider = + ObserveSyncStateUseCaseProvider(coreLogic, userId) + } + + @Provides + fun provideObserveScreenshotCensoringConfigUseCaseProviderFactory( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ObserveScreenshotCensoringConfigUseCaseProvider.Factory = + object : ObserveScreenshotCensoringConfigUseCaseProvider.Factory { + override fun create(userId: UserId): ObserveScreenshotCensoringConfigUseCaseProvider = + ObserveScreenshotCensoringConfigUseCaseProvider(coreLogic, userId) + } + + @Provides + fun provideObserveIfE2EIRequiredDuringLoginUseCaseProviderFactory( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory = + object : ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory { + override fun create(userId: UserId): ObserveIfE2EIRequiredDuringLoginUseCaseProvider = + ObserveIfE2EIRequiredDuringLoginUseCaseProvider(coreLogic, userId) + } + + @Provides + fun provideIsProfileQRCodeEnabledUseCaseProviderFactory( + @KaliumCoreLogic coreLogic: CoreLogic, + ): IsProfileQRCodeEnabledUseCaseProvider.Factory = + object : IsProfileQRCodeEnabledUseCaseProvider.Factory { + override fun create(userId: UserId): IsProfileQRCodeEnabledUseCaseProvider = + IsProfileQRCodeEnabledUseCaseProvider(coreLogic, userId) + } + + @Provides + fun provideObserveSelfUserUseCaseProviderFactory( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ObserveSelfUserUseCaseProvider.Factory = + object : ObserveSelfUserUseCaseProvider.Factory { + override fun create(userId: UserId): ObserveSelfUserUseCaseProvider = + ObserveSelfUserUseCaseProvider(coreLogic, userId) + } + + @Provides + fun provideDeepLinkProcessor( + accountSwitch: AccountSwitchUseCase, + currentSession: CurrentSessionUseCase, + @KaliumCoreLogic coreLogic: CoreLogic, + ): DeepLinkProcessor = + DeepLinkProcessor(accountSwitch, currentSession, coreLogic) + + @Provides + fun provideDeepLinkProcessorLazy(deepLinkProcessor: DeepLinkProcessor): dagger.Lazy = + object : dagger.Lazy { + override fun get(): DeepLinkProcessor = deepLinkProcessor + } + + @Provides + fun provideIntentsProcessor(): IntentsProcessor = + IntentsProcessor(NomadIntentSignatureValidator()) + + @Provides + fun provideIntentsProcessorLazy(intentsProcessor: IntentsProcessor): dagger.Lazy = + object : dagger.Lazy { + override fun get(): IntentsProcessor = intentsProcessor + } + + @Provides + fun provideWireActivityIntentGateway( + deepLinkProcessor: dagger.Lazy, + intentsProcessor: dagger.Lazy, + ): WireActivityIntentGateway = + AndroidWireActivityIntentGateway(deepLinkProcessor, intentsProcessor) + + @Provides + fun provideWireActivityIntentGatewayLazy( + intentGateway: WireActivityIntentGateway, + ): dagger.Lazy = + object : dagger.Lazy { + override fun get(): WireActivityIntentGateway = intentGateway + } + + @Provides + fun provideMonitorSyncWorkUseCase( + @ApplicationContext context: Context, + @KaliumCoreLogic coreLogic: CoreLogic, + observeSessions: ObserveSessionsUseCase, + ): MonitorSyncWorkUseCase = + MonitorSyncWorkUseCase(context, coreLogic, observeSessions) + + @Provides + fun provideIsAnalyticsAvailableUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + analyticsConfiguration: AnalyticsConfiguration, + userDataStoreProvider: UserDataStoreProvider, + ): IsAnalyticsAvailableUseCase = + IsAnalyticsAvailableUseCase(coreLogic, analyticsConfiguration, userDataStoreProvider) + + @Provides + fun provideIsAnalyticsAvailableUseCaseLazy( + isAnalyticsAvailableUseCase: IsAnalyticsAvailableUseCase, + ): dagger.Lazy = + object : dagger.Lazy { + override fun get(): IsAnalyticsAvailableUseCase = isAnalyticsAvailableUseCase + } + + @Provides + fun provideSelfServerConfigUseCaseLazy(selfServerConfig: SelfServerConfigUseCase): dagger.Lazy = + object : dagger.Lazy { + override fun get(): SelfServerConfigUseCase = selfServerConfig + } + + @Provides + fun provideDisableAppLockUseCaseLazy(disableAppLockUseCase: DisableAppLockUseCase): dagger.Lazy = + object : dagger.Lazy { + override fun get(): DisableAppLockUseCase = disableAppLockUseCase + } + + @Provides + fun provideKaliumConfigs(): KaliumConfigs = + KaliumConfigs( + fileRestrictionState = lazy { + if (BuildConfig.FILE_RESTRICTION_ENABLED) { + BuildConfig.FILE_RESTRICTION_LIST.split(",").map { it.trim() }.let { + BuildFileRestrictionState.AllowSome(it) + } + } else { + BuildFileRestrictionState.NoRestriction + } + }, + forceConstantBitrateCalls = BuildConfig.FORCE_CONSTANT_BITRATE_CALLS, + shouldEncryptData = { !BuildConfig.DEBUG || Build.VERSION.SDK_INT < Build.VERSION_CODES.R }, + lowerKeyPackageLimits = BuildConfig.LOWER_KEYPACKAGE_LIMIT, + developmentApiEnabled = BuildConfig.DEVELOPMENT_API_ENABLED, + ignoreSSLCertificatesForUnboundCalls = BuildConfig.IGNORE_SSL_CERTIFICATES, + encryptProteusStorage = true, + guestRoomLink = BuildConfig.ENABLE_GUEST_ROOM_LINK, + selfDeletingMessages = BuildConfig.SELF_DELETING_MESSAGES, + wipeOnCookieInvalid = BuildConfig.WIPE_ON_COOKIE_INVALID, + wipeOnDeviceRemoval = BuildConfig.WIPE_ON_DEVICE_REMOVAL, + wipeOnRootedDevice = BuildConfig.WIPE_ON_ROOTED_DEVICE, + certPinningConfig = BuildConfig.CERTIFICATE_PINNING_CONFIG, + maxRemoteSearchResultCount = BuildConfig.MAX_REMOTE_SEARCH_RESULT_COUNT, + limitTeamMembersFetchDuringSlowSync = BuildConfig.LIMIT_TEAM_MEMBERS_FETCH_DURING_SLOW_SYNC, + isMlsResetEnabled = BuildConfig.IS_MLS_RESET_ENABLED, + collaboraIntegration = BuildConfig.COLLABORA_INTEGRATION_ENABLED, + dbInvalidationControlEnabled = BuildConfig.DB_INVALIDATION_CONTROL_ENABLED, + domainWithFaultyKeysMap = BuildConfig.DOMAIN_REMOVAL_KEYS_FOR_REPAIR, + isDebug = BuildConfig.DEBUG, + ) + + @Provides + fun provideOtherAccountMapper(): OtherAccountMapper = + OtherAccountMapper() + + @CurrentAccount + @Provides + fun provideCurrentSession(@KaliumCoreLogic coreLogic: CoreLogic): UserId = + runBlocking { + when (val result = coreLogic.getGlobalScope().session.currentSession()) { + is CurrentSessionResult.Success -> result.accountInfo.userId + else -> throw IllegalStateException("no current session was found") + } + } + + @Provides + fun provideObserveSyncStateUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ObserveSyncStateUseCase = + coreLogic.getSessionScope(currentAccount).observeSyncState + + @Provides + fun provideUserScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): UserScope = + coreLogic.getSessionScope(currentAccount).users + + @Provides + fun provideClientScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ClientScope = + coreLogic.getSessionScope(currentAccount).client + + @Provides + fun provideDebugScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): DebugScope = + coreLogic.getSessionScope(currentAccount).debug + + @Provides + fun provideConnectionScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ConnectionScope = + coreLogic.getSessionScope(currentAccount).connection + + @Provides + fun provideGetAvatarAssetUseCase(userScope: UserScope): GetAvatarAssetUseCase = + userScope.getPublicAsset + + @Provides + fun provideDeleteAssetUseCase(userScope: UserScope): DeleteAssetUseCase = + userScope.deleteAsset + + @Provides + fun provideUploadUserAvatarUseCase(userScope: UserScope): UploadUserAvatarUseCase = + userScope.uploadUserAvatar + + @Provides + fun provideKaliumFileSystem( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): KaliumFileSystem = + coreLogic.getSessionScope(currentAccount).kaliumFileSystem + + @Provides + fun provideQualifiedIdMapper( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): QualifiedIdMapper = + coreLogic.getSessionScope(currentAccount).qualifiedIdMapper + + @Provides + fun provideAvatarImageManager( + @ApplicationContext context: Context, + ): AvatarImageManager = + AvatarImageManager(context) + + @Provides + fun provideAudioNormalizedLoudnessBuilder(@KaliumCoreLogic coreLogic: CoreLogic): AudioNormalizedLoudnessBuilder = + coreLogic.audioNormalizedLoudnessBuilder + + @ApplicationScope + @Provides + @SingleIn(WireMetroScope::class) + fun provideApplicationCoroutineScope(dispatcherProvider: DispatcherProvider): CoroutineScope = + CoroutineScope(SupervisorJob() + dispatcherProvider.default()) + + @Provides + fun provideMusicMediaPlayer(): MediaPlayer = + MediaPlayer().apply { + setAudioAttributes( + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + ) + } + + @Provides + fun provideAudioManager(@ApplicationContext context: Context): AudioManager = + context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + @Provides + @SingleIn(WireMetroScope::class) + fun provideScreenStateObserver(@ApplicationContext context: Context): ScreenStateObserver = + ScreenStateObserver(context) + + @Provides + @SingleIn(WireMetroScope::class) + fun provideCurrentScreenManager(screenStateObserver: ScreenStateObserver): CurrentScreenManager = + CurrentScreenManager(screenStateObserver) + + @Provides + fun provideCurrentSessionUseCaseLazy(currentSession: CurrentSessionUseCase): dagger.Lazy = + object : dagger.Lazy { + override fun get(): CurrentSessionUseCase = currentSession + } + + @Provides + fun provideSwitchAccountObserver(): SwitchAccountObserver = + SwitchAccountObserver() + + @Provides + fun provideNetworkUtil(@ApplicationContext context: Context): NetworkUtil = + NetworkUtil(context) + + @Provides + fun provideManagedConfigurationsReporter( + @ApplicationContext context: Context, + ): ManagedConfigurationsReporter = + ManagedConfigurationsReporter(context) + + @Provides + fun provideManagedConfigurationsReceiver( + managedConfigurationsManager: ManagedConfigurationsManager, + managedConfigurationsReporter: ManagedConfigurationsReporter, + @KaliumCoreLogic coreLogic: dagger.Lazy, + startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase, + dispatcherProvider: DispatcherProvider, + ): ManagedConfigurationsReceiver = + ManagedConfigurationsReceiver( + managedConfigurationsManager = managedConfigurationsManager, + managedConfigurationsReporter = managedConfigurationsReporter, + coreLogic = coreLogic, + startPersistentWebsocketIfNecessary = startPersistentWebsocketIfNecessary, + dispatcher = dispatcherProvider, + ) + + @Provides + fun provideDynamicReceiversManager( + @ApplicationContext context: Context, + managedConfigurationsReceiver: ManagedConfigurationsReceiver, + ): DynamicReceiversManager = + DynamicReceiversManager( + context = context, + managedConfigurationsReceiver = managedConfigurationsReceiver, + ) + + @Provides + @SingleIn(WireMetroScope::class) + fun provideProximitySensorManager( + @ApplicationContext context: Context, + currentSession: CurrentSessionUseCase, + @KaliumCoreLogic coreLogic: dagger.Lazy, + @ApplicationScope appCoroutineScope: CoroutineScope, + ): ProximitySensorManager = + ProximitySensorManager( + context = context, + currentSession = currentSession, + coreLogic = coreLogic, + appCoroutineScope = appCoroutineScope, + ) + + @Provides + fun provideRecordAudioFileGateway( + @ApplicationContext context: Context, + generateAudioFileWithEffects: GenerateAudioFileWithEffectsUseCase, + ): RecordAudioFileGateway = + AndroidRecordAudioFileGateway( + context = context, + generateAudioFileWithEffects = generateAudioFileWithEffects, + ) + + @Provides + fun provideAudioMediaRecorder( + kaliumFileSystem: KaliumFileSystem, + dispatchers: DispatcherProvider, + ): AudioMediaRecorder = + AudioMediaRecorder( + kaliumFileSystem = kaliumFileSystem, + dispatcherProvider = dispatchers, + ) + + @Provides + fun provideAvatarImageGateway( + avatarImageManager: AvatarImageManager, + dispatchers: DispatcherProvider, + @ApplicationContext context: Context, + ): AvatarImageGateway = + AndroidAvatarImageGateway( + avatarImageManager = avatarImageManager, + dispatchers = dispatchers, + appContext = context, + ) + + @Provides + fun provideSelfQRCodeAssetRepository( + @ApplicationContext context: Context, + kaliumFileSystem: KaliumFileSystem, + dispatchers: DispatcherProvider, + ): SelfQRCodeAssetRepository = + AndroidSelfQRCodeAssetRepository( + context = context, + kaliumFileSystem = kaliumFileSystem, + dispatchers = dispatchers, + ) + + @Provides + fun provideGetFeatureConfigUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): GetFeatureConfigUseCase = + coreLogic.getSessionScope(currentAccount).debug.getFeatureConfig + + @Provides + fun provideGetConversationCryptoStatsUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): GetConversationCryptoStatsUseCase = + coreLogic.getSessionScope(currentAccount).debug.getConversationCryptoStats + + @Provides + fun provideDebugFeedConversationUseCase(debugScope: DebugScope): DebugFeedConversationUseCase = + debugScope.debugFeedConversationUseCase + + @Provides + fun provideGetConversationEpochFromCCUseCase(debugScope: DebugScope): GetConversationEpochFromCCUseCase = + debugScope.getConversationEpochFromCC + + @Provides + fun provideBreakSessionUseCase(debugScope: DebugScope): BreakSessionUseCase = + debugScope.breakSession + + @Provides + fun provideDebugDataInfoProvider(@ApplicationContext context: Context): DebugDataInfoProvider = + AndroidDebugDataInfoProvider(context) + + @Provides + fun provideUpdateApiVersionsScheduler(@KaliumCoreLogic coreLogic: CoreLogic): UpdateApiVersionsScheduler = + coreLogic.getGlobalScope().updateApiVersionsScheduler + + @Provides + fun provideMlsKeyPackageCountUseCase(clientScope: ClientScope): MLSKeyPackageCountUseCase = + clientScope.mlsKeyPackageCountUseCase + + @Provides + fun provideRestartSlowSyncProcessForRecoveryUseCase(clientScope: ClientScope): RestartSlowSyncProcessForRecoveryUseCase = + clientScope.restartSlowSyncProcessForRecoveryUseCase + + @Provides + fun provideCheckCrlRevocationListUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): CheckCrlRevocationListUseCase = + coreLogic.getSessionScope(currentAccount).checkCrlRevocationList + + @Provides + fun provideFetchConversationMLSVerificationStatusUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): FetchConversationMLSVerificationStatusUseCase = + coreLogic.getSessionScope(currentAccount).fetchConversationMLSVerificationStatus + + @Provides + fun provideGetCurrentAnalyticsTrackingIdentifierUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): GetCurrentAnalyticsTrackingIdentifierUseCase = + coreLogic.getSessionScope(currentAccount).getCurrentAnalyticsTrackingIdentifier + + @Provides + fun provideSendFCMTokenUseCase(debugScope: DebugScope): SendFCMTokenUseCase = + debugScope.sendFCMTokenToServer + + @Provides + fun provideObserveIsConsumableNotificationsEnabledUseCase(debugScope: DebugScope): ObserveIsConsumableNotificationsEnabledUseCase = + debugScope.observeIsConsumableNotificationsEnabled + + @Provides + fun provideStartUsingAsyncNotificationsUseCase(debugScope: DebugScope): StartUsingAsyncNotificationsUseCase = + debugScope.startUsingAsyncNotifications + + @Provides + fun provideRepairFaultyRemovalKeysUseCase(debugScope: DebugScope): RepairFaultyRemovalKeysUseCase = + debugScope.repairFaultyRemovalKeysUseCase + + @Provides + fun provideGetDebugE2EICertificateExpirationUseCase(debugScope: DebugScope): GetDebugE2EICertificateExpirationUseCase = + debugScope.getDebugE2EICertificateExpiration + + @Provides + fun provideSetDebugE2EICertificateExpirationUseCase(debugScope: DebugScope): SetDebugE2EICertificateExpirationUseCase = + debugScope.setDebugE2EICertificateExpiration + + @Provides + fun provideChangeProfilingUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ChangeProfilingUseCase = + coreLogic.getSessionScope(currentAccount).debug.changeProfiling + + @Provides + fun provideObserveDatabaseLoggerStateUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ObserveDatabaseLoggerStateUseCase = + coreLogic.getSessionScope(currentAccount).debug.observeDatabaseLoggerState + + @Provides + fun provideObserveIsAppLockEditableUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ObserveIsAppLockEditableUseCase = + coreLogic.getGlobalScope().observeIsAppLockEditableUseCase + + @Provides + fun provideObserveSelfUserUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ObserveSelfUserUseCase = + coreLogic.getSessionScope(currentAccount).users.observeSelfUser + + @Provides + fun provideObserveSelfUserWithTeamUseCase(userScope: UserScope): ObserveSelfUserWithTeamUseCase = + userScope.observeSelfUserWithTeam + + @Provides + fun provideObserveUserInfoUseCase(userScope: UserScope): ObserveUserInfoUseCase = + userScope.observeUserInfo + + @Provides + fun provideUpdateSelfAvailabilityStatusUseCase(userScope: UserScope): UpdateSelfAvailabilityStatusUseCase = + userScope.updateSelfAvailabilityStatus + + @Provides + fun provideCanMigrateFromPersonalToTeamUseCase(userScope: UserScope): CanMigrateFromPersonalToTeamUseCase = + userScope.isPersonalToTeamAccountSupportedByBackend + + @Provides + fun provideMigrateFromPersonalToTeamUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): MigrateFromPersonalToTeamUseCase = + coreLogic.getSessionScope(currentAccount).migrateFromPersonalToTeam + + @Provides + fun provideIsProfileQRCodeEnabledUseCase(userScope: UserScope): IsProfileQRCodeEnabledUseCase = + userScope.isProfileQRCodeEnabled + + @Provides + fun provideObserveValidAccountsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ObserveValidAccountsUseCase = + coreLogic.getGlobalScope().observeValidAccounts + + @Provides + fun provideObserveLegalHoldStateForSelfUserUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ObserveLegalHoldStateForSelfUserUseCase = + coreLogic.getSessionScope(currentAccount).observeLegalHoldForSelfUser + + @Provides + fun provideGetTeamUrlUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): GetTeamUrlUseCase = + coreLogic.getSessionScope(currentAccount).getTeamUrlUseCase + + @Provides + fun provideUserTypeMapper(): UserTypeMapper = + UserTypeMapper() + + @Provides + fun provideUICallParticipantMapper(userTypeMapper: UserTypeMapper): UICallParticipantMapper = + UICallParticipantMapper(userTypeMapper) + + @Provides + fun provideContactMapper(userTypeMapper: UserTypeMapper): ContactMapper = + ContactMapper(userTypeMapper) + + @Provides + fun provideMessageResourceProvider(): MessageResourceProvider = + MessageResourceProvider() + + @Provides + fun provideGetMediaMetadataUseCase(): GetMediaMetadataUseCase = + GetMediaMetadataUseCaseImpl() + + @Provides + fun provideImageUtil(): ImageUtil = + ImageUtil + + @Provides + fun provideTempWritableAttachmentUriProvider( + fileManager: FileManager, + kaliumFileSystem: KaliumFileSystem, + ): TempWritableAttachmentUriProvider = + AndroidTempWritableAttachmentUriProvider( + fileManager = fileManager, + kaliumFileSystem = kaliumFileSystem, + ) + + @Provides + fun provideMessageAttachmentAssetImporter( + handleUriAsset: HandleUriAssetUseCase, + ): MessageAttachmentAssetImporter = + MessageAttachmentAssetImporterImpl(handleUriAsset) + + @Provides + fun provideMessageAttachmentFileGateway( + fileManager: FileManager, + ): MessageAttachmentFileGateway = + MessageAttachmentFileGatewayImpl(fileManager) + + @Provides + fun provideISOFormatter(): ISOFormatter = + ISOFormatter() + + @Provides + fun provideUIAssetMapper(): UIAssetMapper = + UIAssetMapper() + + @Provides + fun provideRegularMessageMapper( + messageResourceProvider: MessageResourceProvider, + isoFormatter: ISOFormatter, + ): RegularMessageMapper = + RegularMessageMapper(messageResourceProvider, isoFormatter) + + @Provides + fun provideSystemMessageContentMapper(messageResourceProvider: MessageResourceProvider): SystemMessageContentMapper = + SystemMessageContentMapper(messageResourceProvider) + + @Provides + fun provideMessageContentMapper( + regularMessageMapper: RegularMessageMapper, + systemMessageContentMapper: SystemMessageContentMapper, + ): MessageContentMapper = + MessageContentMapper(regularMessageMapper, systemMessageContentMapper) + + @Provides + fun provideMessageMapper( + userTypeMapper: UserTypeMapper, + messageContentMapper: MessageContentMapper, + isoFormatter: ISOFormatter, + ): MessageMapper = + MessageMapper(userTypeMapper, messageContentMapper, isoFormatter) + + @Provides + fun provideUIParticipantMapper(userTypeMapper: UserTypeMapper): UIParticipantMapper = + UIParticipantMapper(userTypeMapper) + + @Provides + fun provideGetSelfUserUseCase(userScope: UserScope): GetSelfUserUseCase = + userScope.getSelfUser + + @Provides + fun provideGetMembersE2EICertificateStatusesUseCase(userScope: UserScope): GetMembersE2EICertificateStatusesUseCase = + userScope.getMembersE2EICertificateStatuses + + @Provides + fun provideRefreshUsersWithoutMetadataUseCase(userScope: UserScope): RefreshUsersWithoutMetadataUseCase = + userScope.refreshUsersWithoutMetadata + + @Provides + fun provideGetMLSClientIdentityUseCase(userScope: UserScope): GetMLSClientIdentityUseCase = + userScope.getE2EICertificate + + @Provides + fun provideIsOtherUserE2EIVerifiedUseCase(userScope: UserScope): IsOtherUserE2EIVerifiedUseCase = + userScope.getUserE2eiCertificateStatus + + @Provides + fun provideSelfServerConfigUseCase(userScope: UserScope): SelfServerConfigUseCase = + userScope.serverLinks + + @Provides + fun provideGetDefaultProtocolUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): GetDefaultProtocolUseCase = + coreLogic.getSessionScope(currentAccount).getDefaultProtocol + + @Provides + fun provideAnalyticsConfiguration(): AnalyticsConfiguration = + if (BuildConfig.ANALYTICS_ENABLED) AnalyticsConfiguration.Enabled else AnalyticsConfiguration.Disabled + + @Provides + fun provideObserveReadReceiptsEnabledUseCase(userScope: UserScope): ObserveReadReceiptsEnabledUseCase = + userScope.observeReadReceiptsEnabled + + @Provides + fun providePersistReadReceiptsStatusConfigUseCase(userScope: UserScope): PersistReadReceiptsStatusConfigUseCase = + userScope.persistReadReceiptsStatusConfig + + @Provides + fun provideObserveTypingIndicatorEnabledUseCase(userScope: UserScope): ObserveTypingIndicatorEnabledUseCase = + userScope.observeTypingIndicatorEnabled + + @Provides + fun providePersistTypingIndicatorStatusConfigUseCase(userScope: UserScope): PersistTypingIndicatorStatusConfigUseCase = + userScope.persistTypingIndicatorStatusConfig + + @Provides + fun provideObserveScreenshotCensoringConfigUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ObserveScreenshotCensoringConfigUseCase = + coreLogic.getSessionScope(currentAccount).observeScreenshotCensoringConfig + + @Provides + fun providePersistScreenshotCensoringConfigUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): PersistScreenshotCensoringConfigUseCase = + coreLogic.getSessionScope(currentAccount).persistScreenshotCensoringConfig + + @Provides + fun provideIsPasswordRequiredUseCase(userScope: UserScope): IsPasswordRequiredUseCase = + userScope.isPasswordRequired + + @Provides + fun provideIsReadOnlyAccountUseCase(userScope: UserScope): IsReadOnlyAccountUseCase = + userScope.isReadOnlyAccount + + @Provides + fun provideIsFileSharingEnabledUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): IsFileSharingEnabledUseCase = + coreLogic.getSessionScope(currentAccount).isFileSharingEnabled + + @Provides + fun provideDeleteAccountUseCase(userScope: UserScope): DeleteAccountUseCase = + userScope.deleteAccount + + @Provides + fun provideSetUserHandleUseCase(userScope: UserScope): SetUserHandleUseCase = + userScope.setUserHandle + + @Provides + fun provideValidateUserHandleUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ValidateUserHandleUseCase = + coreLogic.getGlobalScope().validateUserHandleUseCase + + @Provides + fun provideValidatePasswordUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ValidatePasswordUseCase = + coreLogic.getGlobalScope().validatePasswordUseCase + + @Provides + fun provideValidateEmailUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ValidateEmailUseCase = + coreLogic.getGlobalScope().validateEmailUseCase + + @Provides + fun provideValidateSSOCodeUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ValidateSSOCodeUseCase = + coreLogic.getGlobalScope().validateSSOCodeUseCase + + @Provides + fun provideValidateEmailOrSSOCodeUseCase( + validateEmail: ValidateEmailUseCase, + validateSSOCode: ValidateSSOCodeUseCase, + ): ValidateEmailOrSSOCodeUseCase = + ValidateEmailOrSSOCodeUseCase(validateEmail, validateSSOCode) + + @Provides + fun provideNewLoginRecoverableLogoutExceptionDetector(): NewLoginRecoverableLogoutExceptionDetector = + NewLoginRecoverableLogoutExceptionDetector() + + @Provides + fun provideMarkTeamAppLockStatusAsNotifiedUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): MarkTeamAppLockStatusAsNotifiedUseCase = + coreLogic.getSessionScope(currentAccount).markTeamAppLockStatusAsNotified + + @Provides + fun provideUpdateEmailUseCase(userScope: UserScope): UpdateEmailUseCase = + userScope.updateEmail + + @Provides + fun provideUpdateAccentColorUseCase(userScope: UserScope): UpdateAccentColorUseCase = + userScope.updateAccentColor + + @Provides + fun provideUpdateDisplayNameUseCase(userScope: UserScope): UpdateDisplayNameUseCase = + userScope.updateDisplayName + + @Provides + fun provideFinalizeMLSClientAfterE2EIEnrollmentUseCase(userScope: UserScope): FinalizeMLSClientAfterE2EIEnrollment = + userScope.finalizeMLSClientAfterE2EIEnrollment + + @Provides + fun provideDeleteSessionUseCase(@KaliumCoreLogic coreLogic: CoreLogic): DeleteSessionUseCase = + coreLogic.getGlobalScope().deleteSession + + @Provides + fun provideLogoutUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): LogoutUseCase = + coreLogic.getSessionScope(currentAccount).logout + + @Provides + fun provideDeleteClientUseCase(clientScope: ClientScope): DeleteClientUseCase = + clientScope.deleteClient + + @Provides + fun provideGetOrRegisterClientUseCase(clientScope: ClientScope): GetOrRegisterClientUseCase = + clientScope.getOrRegister + + @Provides + fun provideNeedsToRegisterClientUseCase(clientScope: ClientScope): NeedsToRegisterClientUseCase = + clientScope.needsToRegisterClient + + @Provides + fun provideFetchSelfClientsFromRemoteUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): FetchSelfClientsFromRemoteUseCase = + coreLogic.getSessionScope(currentAccount).client.fetchSelfClients + + @Provides + fun provideFetchUsersClientsFromRemoteUseCase(clientScope: ClientScope): FetchUsersClientsFromRemoteUseCase = + clientScope.fetchUsersClients + + @Provides + fun provideClientFingerPrintUseCase(clientScope: ClientScope): ClientFingerprintUseCase = + clientScope.remoteClientFingerPrint + + @Provides + fun provideUpdateClientVerificationStatusUseCase(clientScope: ClientScope): UpdateClientVerificationStatusUseCase = + clientScope.updateClientVerificationStatus + + @Provides + fun provideObserveClientDetailsUseCase(clientScope: ClientScope): ObserveClientDetailsUseCase = + clientScope.observeClientDetailsUseCase + + @Provides + fun provideObserveClientsByUserIdUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ObserveClientsByUserIdUseCase = + coreLogic.getSessionScope(currentAccount).client.getOtherUserClients + + @Provides + fun provideObserveCurrentClientIdUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ObserveCurrentClientIdUseCase = + coreLogic.getSessionScope(currentAccount).client.observeCurrentClientId + + @Provides + fun provideGetUserMlsClientIdentitiesUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): GetUserMlsClientIdentitiesUseCase = + coreLogic.getSessionScope(currentAccount).users.getUserMlsClientIdentities + + @Provides + fun provideIsE2EIEnabledUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): IsE2EIEnabledUseCase = + coreLogic.getSessionScope(currentAccount).isE2EIEnabled + + @Provides + fun provideIsMLSEnabledUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): IsMLSEnabledUseCase = + coreLogic.getSessionScope(currentAccount).isMLSEnabled + + @Provides + fun provideIsWireCellsEnabledUseCase(userScope: UserScope): IsWireCellsEnabledUseCase = + userScope.isWireCellsEnabled + + @Provides + fun provideIsWireCellsEnabledForConversationUseCase(userScope: UserScope): IsWireCellsEnabledForConversationUseCase = + userScope.isWireCellsEnabledForConversation + + @Provides + fun provideForegroundActionsUseCase(userScope: UserScope): ForegroundActionsUseCase = + userScope.foregroundActions + + @Provides + fun provideTeamScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): TeamScope = + coreLogic.getSessionScope(currentAccount).team + + @Provides + fun provideIsSelfATeamMemberUseCase(teamScope: TeamScope): IsSelfATeamMemberUseCase = + teamScope.isSelfATeamMember + + @Provides + fun provideSyncSelfTeamInfoUseCase(teamScope: TeamScope): SyncSelfTeamInfoUseCase = + teamScope.syncSelfTeamInfoUseCase + + @Provides + fun provideConversationScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ConversationScope = + coreLogic.getSessionScope(currentAccount).conversations + + @Provides + fun provideObserveConversationDetailsUseCase(conversationScope: ConversationScope): ObserveConversationDetailsUseCase = + conversationScope.observeConversationDetails + + @Provides + fun provideObserveConversationListDetailsWithEventsUseCase( + conversationScope: ConversationScope, + ): ObserveConversationListDetailsWithEventsUseCase = + conversationScope.observeConversationListDetailsWithEvents + + @Provides + fun provideGetConversationUnreadEventsCountUseCase(conversationScope: ConversationScope): GetConversationUnreadEventsCountUseCase = + conversationScope.getConversationUnreadEventsCountUseCase + + @Provides + fun provideClearUsersTypingEventsUseCase(conversationScope: ConversationScope): ClearUsersTypingEventsUseCase = + conversationScope.clearUsersTypingEvents + + @Provides + fun provideRefreshConversationsWithoutMetadataUseCase( + conversationScope: ConversationScope, + ): RefreshConversationsWithoutMetadataUseCase = + conversationScope.refreshConversationsWithoutMetadata + + @Provides + fun provideObserveArchivedUnreadConversationsCountUseCase( + conversationScope: ConversationScope, + ): ObserveArchivedUnreadConversationsCountUseCase = + conversationScope.observeArchivedUnreadConversationsCount + + @Provides + fun provideObserveArchivedUnreadConversationsCountUseCaseLazy( + observeArchivedUnreadConversationsCount: ObserveArchivedUnreadConversationsCountUseCase, + ): dagger.Lazy = + object : dagger.Lazy { + override fun get(): ObserveArchivedUnreadConversationsCountUseCase = observeArchivedUnreadConversationsCount + } + + @Provides + fun provideNotifyConversationIsOpenUseCase(conversationScope: ConversationScope): NotifyConversationIsOpenUseCase = + conversationScope.notifyConversationIsOpen + + @Provides + fun provideObserveConversationInteractionAvailabilityUseCase( + conversationScope: ConversationScope, + ): ObserveConversationInteractionAvailabilityUseCase = + conversationScope.observeConversationInteractionAvailabilityUseCase + + @Provides + fun provideMembersToMentionUseCase(conversationScope: ConversationScope): MembersToMentionUseCase = + conversationScope.getMembersToMention + + @Provides + fun provideUpdateConversationReadDateUseCase(conversationScope: ConversationScope): UpdateConversationReadDateUseCase = + conversationScope.updateConversationReadDateUseCase + + @Provides + fun provideMarkConversationAsReadLocallyUseCase(conversationScope: ConversationScope): MarkConversationAsReadLocallyUseCase = + conversationScope.markConversationAsReadLocally + + @Provides + fun provideSendTypingEventUseCase(conversationScope: ConversationScope): SendTypingEventUseCase = + conversationScope.sendTypingEvent + + @Provides + fun provideSetUserInformedAboutVerificationUseCase( + conversationScope: ConversationScope, + ): SetUserInformedAboutVerificationUseCase = + conversationScope.setUserInformedAboutVerificationBeforeMessagingUseCase + + @Provides + fun provideObserveDegradedConversationNotifiedUseCase( + conversationScope: ConversationScope, + ): ObserveDegradedConversationNotifiedUseCase = + conversationScope.observeInformAboutVerificationBeforeMessagingFlagUseCase + + @Provides + fun provideSetNotifiedAboutConversationUnderLegalHoldUseCase( + conversationScope: ConversationScope, + ): SetNotifiedAboutConversationUnderLegalHoldUseCase = + conversationScope.setNotifiedAboutConversationUnderLegalHold + + @Provides + fun provideObserveConversationUnderLegalHoldNotifiedUseCase( + conversationScope: ConversationScope, + ): ObserveConversationUnderLegalHoldNotifiedUseCase = + conversationScope.observeConversationUnderLegalHoldNotified + + @Provides + fun provideResetMLSConversationUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ResetMLSConversationUseCase = + coreLogic.getSessionScope(currentAccount).resetMlsConversation + + @Provides + fun provideFetchConversationUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): FetchConversationUseCase = + coreLogic.getSessionScope(currentAccount).fetchConversationUseCase + + @Provides + fun provideObserveUserFoldersUseCase(conversationScope: ConversationScope): ObserveUserFoldersUseCase = + conversationScope.observeUserFolders + + @Provides + fun provideCreateConversationFolderUseCase(conversationScope: ConversationScope): CreateConversationFolderUseCase = + conversationScope.createConversationFolder + + @Provides + fun provideMoveConversationToFolderUseCase(conversationScope: ConversationScope): MoveConversationToFolderUseCase = + conversationScope.moveConversationToFolder + + @Provides + fun provideObserveConversationMembersUseCase(conversationScope: ConversationScope): ObserveConversationMembersUseCase = + conversationScope.observeConversationMembers + + @Provides + fun provideCreateRegularGroupUseCase(conversationScope: ConversationScope): CreateRegularGroupUseCase = + conversationScope.createRegularGroup + + @Provides + fun provideCreateChannelUseCase(conversationScope: ConversationScope): CreateChannelUseCase = + conversationScope.createChannel + + @Provides + fun provideObserveUsersTypingUseCase(conversationScope: ConversationScope): ObserveUsersTypingUseCase = + conversationScope.observeUsersTyping + + @Provides + fun provideObserveUserListByIdUseCase(conversationScope: ConversationScope): ObserveUserListByIdUseCase = + conversationScope.observeUserListById + + @Provides + fun provideObserveConversationRoleForUserUseCase( + observeConversationMembers: ObserveConversationMembersUseCase, + observeConversationDetails: ObserveConversationDetailsUseCase, + observeSelfUser: ObserveSelfUserUseCase, + ): ObserveConversationRoleForUserUseCase = + ObserveConversationRoleForUserUseCase( + observeConversationMembers = observeConversationMembers, + observeConversationDetails = observeConversationDetails, + observeSelfUser = observeSelfUser, + ) + + @Provides + fun provideRemoveMemberFromConversationUseCase(conversationScope: ConversationScope): RemoveMemberFromConversationUseCase = + conversationScope.removeMemberFromConversation + + @Provides + fun provideUpdateConversationMemberRoleUseCase(conversationScope: ConversationScope): UpdateConversationMemberRoleUseCase = + conversationScope.updateConversationMemberRole + + @Provides + fun provideUpdateConversationReceiptModeUseCase(conversationScope: ConversationScope): UpdateConversationReceiptModeUseCase = + conversationScope.updateConversationReceiptMode + + @Provides + fun provideIsOneToOneConversationCreatedUseCase(conversationScope: ConversationScope): IsOneToOneConversationCreatedUseCase = + conversationScope.isOneToOneConversationCreatedUseCase + + @Provides + fun provideAddServiceToConversationUseCase(conversationScope: ConversationScope): AddServiceToConversationUseCase = + conversationScope.addServiceToConversationUseCase + + @Provides + fun provideAddMemberToConversationUseCase(conversationScope: ConversationScope): AddMemberToConversationUseCase = + conversationScope.addMemberToConversationUseCase + + @Provides + fun provideJoinConversationViaCodeUseCase(conversationScope: ConversationScope): JoinConversationViaCodeUseCase = + conversationScope.joinConversationViaCode + + @Provides + fun provideGetOrCreateOneToOneConversationUseCase(conversationScope: ConversationScope): GetOrCreateOneToOneConversationUseCase = + conversationScope.getOrCreateOneToOneConversationUseCase + + @Provides + fun provideObserveParticipantsForConversationUseCase( + observeConversationMembers: ObserveConversationMembersUseCase, + getMembersE2EICertificateStatuses: GetMembersE2EICertificateStatusesUseCase, + uiParticipantMapper: UIParticipantMapper, + dispatchers: DispatcherProvider, + ): ObserveParticipantsForConversationUseCase = + ObserveParticipantsForConversationUseCase( + observeConversationMembers = observeConversationMembers, + getMembersE2EICertificateStatuses = getMembersE2EICertificateStatuses, + uiParticipantMapper = uiParticipantMapper, + dispatchers = dispatchers, + ) + + @Provides + fun provideUpdateConversationArchivedStatusUseCase(conversationScope: ConversationScope): UpdateConversationArchivedStatusUseCase = + conversationScope.updateConversationArchivedStatus + + @Provides + fun provideUpdateConversationMutedStatusUseCase(conversationScope: ConversationScope): UpdateConversationMutedStatusUseCase = + conversationScope.updateConversationMutedStatus + + @Provides + fun provideDeleteTeamConversationUseCase(conversationScope: ConversationScope): DeleteTeamConversationUseCase = + conversationScope.deleteTeamConversation + + @Provides + fun provideMarkConversationAsDeletedLocallyUseCase(conversationScope: ConversationScope): MarkConversationAsDeletedLocallyUseCase = + conversationScope.markConversationAsDeletedLocallyUseCase + + @Provides + fun provideLeaveConversationUseCase(conversationScope: ConversationScope): LeaveConversationUseCase = + conversationScope.leaveConversation + + @Provides + fun providePromoteAdminAndLeaveConversationUseCase( + conversationScope: ConversationScope, + ): PromoteAdminAndLeaveConversationUseCase = + conversationScope.promoteAdminAndLeaveConversation + + @Provides + fun provideObserveEligibleMembersForConversationAdminRoleUseCase( + conversationScope: ConversationScope, + ): ObserveEligibleMembersForConversationAdminRoleUseCase = + conversationScope.observeEligibleMembersForConversationAdminRole + + @Provides + fun provideCheckConversationLeaveConditionsUseCase(conversationScope: ConversationScope): CheckConversationLeaveConditionsUseCase = + conversationScope.checkConversationLeaveConditions + + @Provides + fun provideClearConversationContentUseCase(conversationScope: ConversationScope): ClearConversationContentUseCase = + conversationScope.clearConversationContent + + @Provides + fun provideRenameConversationUseCase(conversationScope: ConversationScope): RenameConversationUseCase = + conversationScope.renameConversation + + @Provides + fun provideUpdateMessageTimerUseCase(conversationScope: ConversationScope): UpdateMessageTimerUseCase = + conversationScope.updateMessageTimer + + @Provides + fun provideUpdateConversationAccessRoleUseCase(conversationScope: ConversationScope): UpdateConversationAccessRoleUseCase = + conversationScope.updateConversationAccess + + @Provides + fun provideChangeAccessForAppsInConversationUseCase(conversationScope: ConversationScope): ChangeAccessForAppsInConversationUseCase = + conversationScope.changeAccessForAppsInConversation + + @Provides + fun provideCanCreatePasswordProtectedLinksUseCase(conversationScope: ConversationScope): CanCreatePasswordProtectedLinksUseCase = + conversationScope.canCreatePasswordProtectedLinks + + @Provides + fun provideGenerateGuestRoomLinkUseCase(conversationScope: ConversationScope): GenerateGuestRoomLinkUseCase = + conversationScope.generateGuestRoomLink + + @Provides + fun provideRevokeGuestRoomLinkUseCase(conversationScope: ConversationScope): RevokeGuestRoomLinkUseCase = + conversationScope.revokeGuestRoomLink + + @Provides + fun provideObserveGuestRoomLinkUseCase(conversationScope: ConversationScope): ObserveGuestRoomLinkUseCase = + conversationScope.observeGuestRoomLink + + @Provides + fun provideSyncConversationCodeUseCase(conversationScope: ConversationScope): SyncConversationCodeUseCase = + conversationScope.syncConversationCode + + @Provides + fun provideObserveGuestRoomLinkFeatureFlagUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ObserveGuestRoomLinkFeatureFlagUseCase = + coreLogic.getSessionScope(currentAccount).observeGuestRoomLinkFeatureFlag + + @Provides + fun provideRandomPassword(): RandomPassword = + RandomPassword() + + @Provides + fun provideChannelsScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ChannelsScope = + coreLogic.getSessionScope(currentAccount).channels + + @Provides + fun provideUpdateChannelAddPermissionUseCase(channelsScope: ChannelsScope): UpdateChannelAddPermissionUseCase = + channelsScope.updateChannelAddPermission + + @Provides + fun provideObserveChannelsCreationPermissionUseCase( + channelsScope: ChannelsScope, + ): ObserveChannelsCreationPermissionUseCase = + channelsScope.observeChannelsCreationPermissionUseCase + + @Provides + fun provideSendConnectionRequestUseCase(connectionScope: ConnectionScope): SendConnectionRequestUseCase = + connectionScope.sendConnectionRequest + + @Provides + fun provideCancelConnectionRequestUseCase(connectionScope: ConnectionScope): CancelConnectionRequestUseCase = + connectionScope.cancelConnectionRequest + + @Provides + fun provideIgnoreConnectionRequestUseCase(connectionScope: ConnectionScope): IgnoreConnectionRequestUseCase = + connectionScope.ignoreConnectionRequest + + @Provides + fun provideAcceptConnectionRequestUseCase(connectionScope: ConnectionScope): AcceptConnectionRequestUseCase = + connectionScope.acceptConnectionRequest + + @Provides + fun provideBlockUserUseCase(connectionScope: ConnectionScope): BlockUserUseCase = + connectionScope.blockUser + + @Provides + fun provideUnblockUserUseCase(connectionScope: ConnectionScope): UnblockUserUseCase = + connectionScope.unblockUser + + @Provides + fun provideSearchScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): SearchScope = + coreLogic.getSessionScope(currentAccount).search + + @Provides + fun provideSearchUsersUseCase(searchScope: SearchScope): SearchUsersUseCase = + searchScope.searchUsers + + @Provides + fun provideSearchByHandleUseCase(searchScope: SearchScope): SearchByHandleUseCase = + searchScope.searchByHandle + + @Provides + fun provideFederatedSearchParser(searchScope: SearchScope): FederatedSearchParser = + searchScope.federatedSearchParser + + @Provides + fun provideIsFederationSearchAllowedUseCase(searchScope: SearchScope): IsFederationSearchAllowedUseCase = + searchScope.isFederationSearchAllowedUseCase + + @Provides + fun provideAppScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): AppScope = + coreLogic.getSessionScope(currentAccount).apps + + @Provides + fun provideGetAppByIdUseCase(appScope: AppScope): GetAppByIdUseCase = + appScope.getAppById + + @Provides + fun provideObserveIsAppMemberUseCase(appScope: AppScope): ObserveIsAppMemberUseCase = + appScope.observeIsAppMember + + @Provides + fun provideSearchAppsByNameUseCase(appScope: AppScope): SearchAppsByNameUseCase = + appScope.searchAppsByName + + @Provides + fun provideObserveAllAppsUseCase(appScope: AppScope): ObserveAllAppsUseCase = + appScope.observeAllApps + + @Provides + fun provideServiceScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ServiceScope = + coreLogic.getSessionScope(currentAccount).service + + @Provides + fun provideGetServiceByIdUseCase(serviceScope: ServiceScope): GetServiceByIdUseCase = + serviceScope.getServiceById + + @Provides + fun provideObserveIsServiceMemberUseCase(serviceScope: ServiceScope): ObserveIsServiceMemberUseCase = + serviceScope.observeIsServiceMember + + @Provides + fun provideObserveAllServicesUseCase(serviceScope: ServiceScope): ObserveAllServicesUseCase = + serviceScope.observeAllServices + + @Provides + fun provideSearchServicesByNameUseCase(serviceScope: ServiceScope): SearchServicesByNameUseCase = + serviceScope.searchServicesByName + + @Provides + fun provideSyncServicesUseCase(serviceScope: ServiceScope): SyncServicesUseCase = + serviceScope.syncServices + + @Provides + fun provideObserveIsAppsAllowedForUsageUseCase(serviceScope: ServiceScope): ObserveIsAppsAllowedForUsageUseCase = + serviceScope.observeIsAppsAllowedForUsage + + @Provides + fun provideMessageScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): MessageScope = + coreLogic.getSessionScope(currentAccount).messages + + @Provides + fun provideGetMessageAssetUseCase(messageScope: MessageScope): GetMessageAssetUseCase = + messageScope.getAssetMessage + + @Provides + fun provideDeleteMessageUseCase(messageScope: MessageScope): DeleteMessageUseCase = + messageScope.deleteMessage + + @Provides + fun provideGetMessageByIdUseCase(messageScope: MessageScope): GetMessageByIdUseCase = + messageScope.getMessageById + + @Provides + fun provideUpdateAssetMessageTransferStatusUseCase(messageScope: MessageScope): UpdateAssetMessageTransferStatusUseCase = + messageScope.updateAssetMessageTransferStatus + + @Provides + fun provideObserveAssetStatusesUseCase(messageScope: MessageScope): ObserveAssetStatusesUseCase = + messageScope.observeAssetStatuses + + @Provides + fun provideFetchOlderNomadMessagesByConversationUseCase(messageScope: MessageScope): FetchOlderNomadMessagesByConversationUseCase = + messageScope.fetchOlderMessagesByConversationId + + @Provides + fun provideToggleReactionUseCase(messageScope: MessageScope): ToggleReactionUseCase = + messageScope.toggleReaction + + @Provides + fun provideResetSessionUseCase(messageScope: MessageScope): ResetSessionUseCase = + messageScope.resetSession + + @Provides + fun provideGetSearchedConversationMessagePositionUseCase(messageScope: MessageScope): GetSearchedConversationMessagePositionUseCase = + messageScope.getSearchedConversationMessagePosition + + @Provides + fun provideSendButtonActionMessageUseCase(messageScope: MessageScope): SendButtonActionMessageUseCase = + messageScope.sendButtonActionMessage + + @Provides + fun provideEnqueueMessageSelfDeletionUseCase(messageScope: MessageScope): EnqueueMessageSelfDeletionUseCase = + messageScope.enqueueMessageSelfDeletion + + @Provides + fun provideScheduleNewAssetMessageUseCase(messageScope: MessageScope): ScheduleNewAssetMessageUseCase = + messageScope.sendAssetMessage + + @Provides + fun provideSendTextMessageUseCase(messageScope: MessageScope): SendTextMessageUseCase = + messageScope.sendTextMessage + + @Provides + fun provideSendMultipartMessageUseCase(messageScope: MessageScope): SendMultipartMessageUseCase = + messageScope.sendMultipartMessage + + @Provides + fun provideSendEditTextMessageUseCase(messageScope: MessageScope): SendEditTextMessageUseCase = + messageScope.sendEditTextMessage + + @Provides + fun provideSendEditMultipartMessageUseCase(messageScope: MessageScope): SendEditMultipartMessageUseCase = + messageScope.sendEditMultipartMessage + + @Provides + fun provideRetryFailedMessageUseCase(messageScope: MessageScope): RetryFailedMessageUseCase = + messageScope.retryFailedMessage + + @Provides + fun provideSendKnockUseCase(messageScope: MessageScope): SendKnockUseCase = + messageScope.sendKnock + + @Provides + fun provideSendLocationUseCase(messageScope: MessageScope): SendLocationUseCase = + messageScope.sendLocation + + @Provides + fun provideRemoveMessageDraftUseCase(messageScope: MessageScope): RemoveMessageDraftUseCase = + messageScope.removeMessageDraftUseCase + + @Provides + fun provideGetMessageDraftUseCase(messageScope: MessageScope): GetMessageDraftUseCase = + messageScope.getMessageDraftUseCase + + @Provides + fun provideSaveMessageDraftUseCase(messageScope: MessageScope): SaveMessageDraftUseCase = + messageScope.saveMessageDraftUseCase + + @Provides + fun provideGetPaginatedFlowOfMessagesByConversationUseCase( + messageScope: MessageScope, + ): GetPaginatedFlowOfMessagesByConversationUseCase = + messageScope.getPaginatedFlowOfMessagesByConversation + + @Provides + fun provideGetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase( + messageScope: MessageScope, + ): GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase = + messageScope.getPaginatedFlowOfMessagesBySearchQueryAndConversation + + @Provides + fun provideGetPaginatedFlowOfAssetMessageByConversationIdUseCase( + messageScope: MessageScope, + ): GetPaginatedFlowOfAssetMessageByConversationIdUseCase = + messageScope.getPaginatedFlowOfAssetMessageByConversationId + + @Provides + fun provideObservePaginatedAssetImageMessages(messageScope: MessageScope): ObservePaginatedAssetImageMessages = + messageScope.observePaginatedImageAssetMessageByConversationId + + @Provides + fun provideObserveMessageReactionsUseCase(messageScope: MessageScope): ObserveMessageReactionsUseCase = + messageScope.observeMessageReactions + + @Provides + fun provideObserveMessageReceiptsUseCase(messageScope: MessageScope): ObserveMessageReceiptsUseCase = + messageScope.observeMessageReceipts + + @Provides + fun provideObserveMessageByIdUseCase(messageScope: MessageScope): ObserveMessageByIdUseCase = + messageScope.observeMessageById + + @Provides + fun provideCellsScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): CellsScope = + coreLogic.getSessionScope(currentAccount).cells + + @Provides + fun provideGetMessageAttachmentUseCase(cellsScope: CellsScope): GetMessageAttachmentUseCase = + cellsScope.getMessageAttachmentUseCase + + @Provides + fun provideGetPaginatedFilesFlowUseCase(cellsScope: CellsScope): GetPaginatedFilesFlowUseCase = + cellsScope.paginatedFilesFlowUseCase + + @Provides + fun provideGetPaginatedCellConversationsFlowUseCase(cellsScope: CellsScope): GetPaginatedCellConversationsFlowUseCase = + cellsScope.paginatedConversationsFlowUseCase + + @Provides + fun provideDeleteCellAssetUseCase(cellsScope: CellsScope): DeleteCellAssetUseCase = + cellsScope.deleteCellAssetUseCase + + @Provides + fun provideCreateFolderUseCase(cellsScope: CellsScope): CreateFolderUseCase = + cellsScope.createFolderUseCase + + @Provides + fun provideCreatePresentationFileUseCase(cellsScope: CellsScope): CreatePresentationFileUseCase = + cellsScope.createPresentationFileUseCase + + @Provides + fun provideCreateDocumentFileUseCase(cellsScope: CellsScope): CreateDocumentFileUseCase = + cellsScope.createDocumentFileUseCase + + @Provides + fun provideCreateSpreadsheetFileUseCase(cellsScope: CellsScope): CreateSpreadsheetFileUseCase = + cellsScope.createSpreadsheetFileUseCase + + @Provides + fun provideMoveNodeUseCase(cellsScope: CellsScope): MoveNodeUseCase = + cellsScope.moveNodeUseCase + + @Provides + fun provideGetFoldersUseCase(cellsScope: CellsScope): GetFoldersUseCase = + cellsScope.getFoldersUseCase + + @Provides + fun provideGetAllTagsUseCase(cellsScope: CellsScope): GetAllTagsUseCase = + cellsScope.getAllTags + + @Provides + fun provideUpdateNodeTagsUseCase(cellsScope: CellsScope): UpdateNodeTagsUseCase = + cellsScope.updateNodeTagsUseCase + + @Provides + fun provideRemoveNodeTagsUseCase(cellsScope: CellsScope): RemoveNodeTagsUseCase = + cellsScope.removeNodeTagsUseCase + + @Provides + fun provideRenameNodeUseCase(cellsScope: CellsScope): RenameNodeUseCase = + cellsScope.renameNodeUseCase + + @Provides + fun provideGetOwnersUseCase(cellsScope: CellsScope): GetOwnersUseCase = + cellsScope.getOwnersUseCase + + @Provides + fun provideCreatePublicLinkUseCase(cellsScope: CellsScope): CreatePublicLinkUseCase = + cellsScope.createPublicLinkUseCase + + @Provides + fun provideGetPublicLinkUseCase(cellsScope: CellsScope): GetPublicLinkUseCase = + cellsScope.getPublicLinkUseCase + + @Provides + fun provideDeletePublicLinkUseCase(cellsScope: CellsScope): DeletePublicLinkUseCase = + cellsScope.deletePublicLinkUseCase + + @Provides + fun provideCreatePublicLinkPasswordUseCase(cellsScope: CellsScope): CreatePublicLinkPasswordUseCase = + cellsScope.createPublicLinkPasswordUseCase + + @Provides + fun provideUpdatePublicLinkPasswordUseCase(cellsScope: CellsScope): UpdatePublicLinkPasswordUseCase = + cellsScope.updatePublicLinkPasswordUseCase + + @Provides + fun provideGetPublicLinkPasswordUseCase(cellsScope: CellsScope): GetPublicLinkPasswordUseCase = + cellsScope.getPublicLinkPassword + + @Provides + fun provideSetPublicLinkExpirationUseCase(cellsScope: CellsScope): SetPublicLinkExpirationUseCase = + cellsScope.setPublicLinkExpiration + + @Provides + fun provideGetNodeVersionsUseCase(cellsScope: CellsScope): GetNodeVersionsUseCase = + cellsScope.getNodeVersions + + @Provides + fun provideRestoreNodeVersionUseCase(cellsScope: CellsScope): RestoreNodeVersionUseCase = + cellsScope.restoreNodeVersion + + @Provides + fun provideDownloadCellVersionUseCase(cellsScope: CellsScope): DownloadCellVersionUseCase = + cellsScope.downloadCellVersion + + @Provides + fun provideRestoreNodeFromRecycleBinUseCase(cellsScope: CellsScope): RestoreNodeFromRecycleBinUseCase = + cellsScope.restoreNodeFromRecycleBin + + @Provides + fun provideIsAtLeastOneCellAvailableUseCase(cellsScope: CellsScope): IsAtLeastOneCellAvailableUseCase = + cellsScope.isCellAvailable + + @Provides + fun provideObserveAttachmentDraftsUseCase(cellsScope: CellsScope): ObserveAttachmentDraftsUseCase = + cellsScope.observeAttachments + + @Provides + fun provideAddAttachmentDraftUseCase(cellsScope: CellsScope): AddAttachmentDraftUseCase = + cellsScope.addAttachment + + @Provides + fun provideRemoveAttachmentDraftUseCase(cellsScope: CellsScope): RemoveAttachmentDraftUseCase = + cellsScope.removeAttachment + + @Provides + fun provideRetryAttachmentUploadUseCase(cellsScope: CellsScope): RetryAttachmentUploadUseCase = + cellsScope.retryAttachmentUpload + + @Provides + fun provideCellUploadManager(cellsScope: CellsScope): CellUploadManager = + cellsScope.uploadManager + + @Provides + fun provideGetCellFileUseCase(cellsScope: CellsScope): GetCellFileUseCase = + cellsScope.getCellFileUseCase + + @Provides + fun provideDownloadCellFileUseCase(cellsScope: CellsScope): DownloadCellFileUseCase = + cellsScope.downloadCellFile + + @Provides + fun provideRefreshCellAssetStateUseCase(cellsScope: CellsScope): RefreshCellAssetStateUseCase = + cellsScope.refreshAsset + + @Provides + fun provideGetEditorUrlUseCase(cellsScope: CellsScope): GetEditorUrlUseCase = + cellsScope.getEditorUrl + + @Provides + fun provideGetWireCellConfigurationUseCase(cellsScope: CellsScope): GetWireCellConfigurationUseCase = + cellsScope.getCellConfig + + @Provides + fun provideCellFileExternalActions(androidCellFileExternalActions: AndroidCellFileExternalActions): CellFileExternalActions = + androidCellFileExternalActions + + @Provides + fun provideCellAssetRefreshHelper( + refreshAsset: RefreshCellAssetStateUseCase, + kaliumConfigs: KaliumConfigs, + ): CellAssetRefreshHelper = + CellAssetRefreshHelper( + refreshAsset = refreshAsset, + featureFlags = kaliumConfigs, + ) + + @Provides + fun provideFileManager(@ApplicationContext context: Context): FileManager = + FileManager(context) + + @Provides + fun provideTimeZoneProvider(): TimeZoneProvider = + TimeZoneProvider() + + @Provides + fun provideConversationAssetFileGateway(fileManager: FileManager): ConversationAssetFileGateway = + AndroidConversationAssetFileGateway(fileManager) + + @Provides + fun provideUiTextResolver(@ApplicationContext context: Context): UiTextResolver = + AndroidUiTextResolver(context) + + @Provides + fun provideGetAssetSizeLimitUseCase(userScope: UserScope): GetAssetSizeLimitUseCase = + userScope.getAssetSizeLimit + + @Provides + fun provideImagesPreviewAssetImporter( + handleUriAssetUseCase: HandleUriAssetUseCase, + dispatchers: DispatcherProvider, + ): ImagesPreviewAssetImporter = + ImagesPreviewAssetImporterImpl( + handleUriAsset = handleUriAssetUseCase, + dispatchers = dispatchers, + ) + + @Provides + fun provideImportMediaAssetImporter( + handleUriAssetUseCase: HandleUriAssetUseCase, + dispatchers: DispatcherProvider, + ): ImportMediaAssetImporter = + ImportMediaAssetImporterImpl( + handleUriAsset = handleUriAssetUseCase, + dispatchers = dispatchers, + ) + + @Provides + fun provideGetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase( + conversationScope: ConversationScope, + ): GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase = + conversationScope.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery + + @Provides + fun provideObserveConversationsFromFolderUseCase(conversationScope: ConversationScope): ObserveConversationsFromFolderUseCase = + conversationScope.observeConversationsFromFolder + + @Provides + fun provideGetFavoriteFolderUseCase(conversationScope: ConversationScope): GetFavoriteFolderUseCase = + conversationScope.getFavoriteFolder + + @Provides + fun provideAddConversationToFavoritesUseCase(conversationScope: ConversationScope): AddConversationToFavoritesUseCase = + conversationScope.addConversationToFavorites + + @Provides + fun provideRemoveConversationFromFavoritesUseCase(conversationScope: ConversationScope): RemoveConversationFromFavoritesUseCase = + conversationScope.removeConversationFromFavorites + + @Provides + fun provideRemoveConversationFromFolderUseCase(conversationScope: ConversationScope): RemoveConversationFromFolderUseCase = + conversationScope.removeConversationFromFolder + + @Provides + fun provideObserveSelfDeletionTimerSettingsForConversationUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ObserveSelfDeletionTimerSettingsForConversationUseCase = + coreLogic.getSessionScope(currentAccount).observeSelfDeletingMessages + + @Provides + fun providePersistNewSelfDeletingMessagesUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): PersistNewSelfDeletionTimerUseCase = + coreLogic.getSessionScope(currentAccount).persistNewSelfDeletionStatus + + @Provides + fun provideBackupScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): BackupScope = + coreLogic.getSessionScope(currentAccount).backup + + @Provides + fun provideCreateBackupUseCase(backupScope: BackupScope): CreateBackupUseCase = + backupScope.create + + @OptIn(DelicateKaliumApi::class) + @Provides + fun provideCreateObfuscatedCopyUseCase(backupScope: BackupScope): CreateObfuscatedCopyUseCase = + backupScope.createUnEncryptedCopy + + @Provides + fun provideVerifyBackupUseCase(backupScope: BackupScope): VerifyBackupUseCase = + backupScope.verify + + @Provides + fun provideRestoreBackupUseCase(backupScope: BackupScope): RestoreBackupUseCase = + backupScope.restore + + @Provides + fun provideCreateMpBackupUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): CreateMPBackupUseCase = + coreLogic.getSessionScope(currentAccount).multiPlatformBackup.create + + @Provides + fun provideRestoreMpBackupUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): RestoreMPBackupUseCase = + coreLogic.getSessionScope(currentAccount).multiPlatformBackup.restore + + @Provides + fun provideMpBackupSettings(): MPBackupSettings = + if (BuildConfig.ENABLE_CROSSPLATFORM_BACKUP) { + MPBackupSettings.Enabled + } else { + MPBackupSettings.Disabled + } + + @Provides + fun provideBackupFileGateway( + fileManager: FileManager, + kaliumFileSystem: KaliumFileSystem, + dispatchers: DispatcherProvider, + ): BackupFileGateway = + AndroidBackupFileGateway( + fileManager = fileManager, + kaliumFileSystem = kaliumFileSystem, + dispatcher = dispatchers, + ) + + @Provides + fun provideExportObfuscatedCopyFileGateway( + gateway: AndroidExportObfuscatedCopyFileGateway, + ): ExportObfuscatedCopyFileGateway = gateway + + @Provides + @SingleIn(WireMetroScope::class) + fun provideWorkManager(@ApplicationContext context: Context): WorkManager = + WorkManager.getInstance(context) + + @Provides + @SingleIn(WireMetroScope::class) + fun provideWireWorkerFactory( + wireNotificationManager: WireNotificationManager, + notificationChannelsManager: NotificationChannelsManager, + startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase, + @KaliumCoreLogic coreLogic: CoreLogic, + ): WireWorkerFactory = + WireWorkerFactory( + wireNotificationManager = wireNotificationManager, + notificationChannelsManager = notificationChannelsManager, + startPersistentWebsocketIfNecessary = startPersistentWebsocketIfNecessary, + coreLogic = coreLogic, + ) + + @Provides + @SingleIn(WireMetroScope::class) + @Suppress("LongParameterList") + fun provideGlobalObserversManager( + dispatcherProvider: DispatcherProvider, + @KaliumCoreLogic coreLogic: CoreLogic, + notificationManager: WireNotificationManager, + notificationChannelsManager: NotificationChannelsManager, + userDataStoreProvider: UserDataStoreProvider, + currentScreenManager: CurrentScreenManager, + ): GlobalObserversManager = + GlobalObserversManager( + dispatcherProvider = dispatcherProvider, + coreLogic = coreLogic, + notificationManager = notificationManager, + notificationChannelsManager = notificationChannelsManager, + userDataStoreProvider = userDataStoreProvider, + currentScreenManager = currentScreenManager, + ) + + @Provides + fun provideGetSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): GetSessionsUseCase = + coreLogic.getGlobalScope().session.allSessions + + @Provides + fun provideUpdateCurrentSessionUseCase(@KaliumCoreLogic coreLogic: CoreLogic): UpdateCurrentSessionUseCase = + coreLogic.getGlobalScope().session.updateCurrentSession + + @Provides + fun provideDoesValidNomadAccountExistUseCase(@KaliumCoreLogic coreLogic: CoreLogic): DoesValidNomadAccountExistUseCase = + coreLogic.getGlobalScope().doesValidNomadAccountExist + + @Provides + fun provideCallsScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): CallsScope = + coreLogic.getSessionScope(currentAccount).calls + + @Provides + fun provideObserveEstablishedCallsUseCase(callsScope: CallsScope): ObserveEstablishedCallsUseCase = + callsScope.establishedCall + + @Provides + fun provideObserveLastActiveCallWithSortedParticipantsUseCase( + callsScope: CallsScope, + ): ObserveLastActiveCallWithSortedParticipantsUseCase = + callsScope.observeLastActiveCallWithSortedParticipants + + @Provides + fun provideObserveOngoingCallsUseCase(callsScope: CallsScope): ObserveOngoingCallsUseCase = + callsScope.observeOngoingCalls + + @Provides + fun provideObserveOutgoingCallUseCase(callsScope: CallsScope): ObserveOutgoingCallUseCase = + callsScope.observeOutgoingCall + + @Provides + fun provideGetIncomingCallsUseCase(callsScope: CallsScope): GetIncomingCallsUseCase = + callsScope.getIncomingCalls + + @Provides + fun provideRejectCallUseCase(callsScope: CallsScope): RejectCallUseCase = + callsScope.rejectCall + + @Provides + fun provideAnswerCallUseCase(callsScope: CallsScope): AnswerCallUseCase = + callsScope.answerCall + + @Provides + fun provideEndCallUseCase(callsScope: CallsScope): EndCallUseCase = + callsScope.endCall + + @Provides + fun provideStartCallUseCase(callsScope: CallsScope): StartCallUseCase = + callsScope.startCall + + @Provides + fun provideIsLastCallClosedUseCase(callsScope: CallsScope): IsLastCallClosedUseCase = + callsScope.isLastCallClosed + + @Provides + fun provideMuteCallUseCase(callsScope: CallsScope): MuteCallUseCase = + callsScope.muteCall + + @Provides + fun provideUnMuteCallUseCase(callsScope: CallsScope): UnMuteCallUseCase = + callsScope.unMuteCall + + @Provides + fun provideSetVideoPreviewUseCase(callsScope: CallsScope): SetVideoPreviewUseCase = + callsScope.setVideoPreview + + @Provides + fun provideSetUIRotationUseCase(callsScope: CallsScope): SetUIRotationUseCase = + callsScope.setUIRotation + + @Provides + fun provideFlipToFrontCameraUseCase(callsScope: CallsScope): FlipToFrontCameraUseCase = + callsScope.flipToFrontCamera + + @Provides + fun provideFlipToBackCameraUseCase(callsScope: CallsScope): FlipToBackCameraUseCase = + callsScope.flipToBackCamera + + @Provides + fun provideTurnLoudSpeakerOffUseCase(callsScope: CallsScope): TurnLoudSpeakerOffUseCase = + callsScope.turnLoudSpeakerOff + + @Provides + fun provideTurnLoudSpeakerOnUseCase(callsScope: CallsScope): TurnLoudSpeakerOnUseCase = + callsScope.turnLoudSpeakerOn + + @Provides + fun provideObserveSpeakerUseCase(callsScope: CallsScope): ObserveSpeakerUseCase = + callsScope.observeSpeaker + + @Provides + fun provideUpdateVideoStateUseCase(callsScope: CallsScope): UpdateVideoStateUseCase = + callsScope.updateVideoState + + @Provides + fun provideRequestVideoStreamsUseCase(callsScope: CallsScope): RequestVideoStreamsUseCase = + callsScope.requestVideoStreams + + @Provides + fun provideSetVideoSendStateUseCase(callsScope: CallsScope): SetVideoSendStateUseCase = + callsScope.setVideoSendState + + @Provides + fun provideIsEligibleToStartCallUseCase(callsScope: CallsScope): IsEligibleToStartCallUseCase = + callsScope.isEligibleToStartCall + + @Provides + fun provideObserveConferenceCallingEnabledUseCase(callsScope: CallsScope): ObserveConferenceCallingEnabledUseCase = + callsScope.observeConferenceCallingEnabled + + @Provides + fun provideObserveInCallReactionsUseCase(callsScope: CallsScope): ObserveInCallReactionsUseCase = + callsScope.observeInCallReactions + + @Provides + fun provideObserveCallQualityDataUseCase(callsScope: CallsScope): ObserveCallQualityDataUseCase = + callsScope.observeCallQualityData + + @Provides + fun provideSetCallQualityIntervalUseCase(callsScope: CallsScope): SetCallQualityIntervalUseCase = + callsScope.setCallQualityInterval + + @Provides + fun provideObserveCallModerationActionsUseCase(callsScope: CallsScope): ObserveCallModerationActionsUseCase = + callsScope.observeCallModerationActions + + @Provides + fun provideSendInCallReactionUseCase(messageScope: MessageScope): SendInCallReactionUseCase = + messageScope.sendInCallReactionUseCase + + @Provides + fun provideNetworkStateObserver(@KaliumCoreLogic coreLogic: CoreLogic): NetworkStateObserver = + coreLogic.networkStateObserver + + @Provides + fun provideWireSessionImageLoader( + @ApplicationContext context: Context, + getAvatarAsset: GetAvatarAssetUseCase, + deleteAsset: DeleteAssetUseCase, + getMessageAsset: GetMessageAssetUseCase, + networkStateObserver: NetworkStateObserver, + ): WireSessionImageLoader = + WireSessionImageLoader.Factory( + context = context, + getAvatarAsset = getAvatarAsset, + deleteAsset = deleteAsset, + networkStateObserver = networkStateObserver, + getPrivateAsset = getMessageAsset, + ).newImageLoader() + + @Provides + @Suppress("LongParameterList") + fun provideHangUpCallUseCase( + @ApplicationScope coroutineScope: CoroutineScope, + observeEstablishedCalls: ObserveEstablishedCallsUseCase, + observeSpeaker: ObserveSpeakerUseCase, + endCall: EndCallUseCase, + muteCall: MuteCallUseCase, + turnLoudSpeakerOff: TurnLoudSpeakerOffUseCase, + flipToFrontCamera: FlipToFrontCameraUseCase, + callRinger: CallRinger, + ): HangUpCallUseCase = + HangUpCallUseCase( + coroutineScope = coroutineScope, + observeEstablishedCalls = observeEstablishedCalls, + observeSpeaker = observeSpeaker, + endCall = endCall, + muteCall = muteCall, + turnLoudSpeakerOff = turnLoudSpeakerOff, + flipToFrontCamera = flipToFrontCamera, + callRinger = callRinger, + ) + + @Provides + fun provideCallServiceManager(@KaliumCoreLogic coreLogic: CoreLogic): CallServiceManager = + CallServiceManager(coreLogic) + + @Provides + fun providePersistentWebSocketServiceDependencies( + @KaliumCoreLogic coreLogic: CoreLogic, + dispatcherProvider: DispatcherProvider, + notificationManager: WireNotificationManager, + notificationChannelsManager: NotificationChannelsManager, + ): PersistentWebSocketService.Dependencies = + PersistentWebSocketService.Dependencies( + coreLogic = coreLogic, + dispatcherProvider = dispatcherProvider, + notificationManager = notificationManager, + notificationChannelsManager = notificationChannelsManager, + ) + + @Provides + fun provideCallServiceDependencies( + lifecycleManager: CallServiceManager, + callNotificationManager: CallNotificationManager, + dispatcherProvider: DispatcherProvider, + ): CallService.Dependencies = + CallService.Dependencies( + lifecycleManager = lifecycleManager, + callNotificationManager = callNotificationManager, + dispatcherProvider = dispatcherProvider, + ) + + @Provides + fun providePlayingAudioMessageServiceDependencies( + dispatcherProvider: DispatcherProvider, + audioMessagePlayer: ConversationAudioMessagePlayer, + ): PlayingAudioMessageService.Dependencies = + PlayingAudioMessageService.Dependencies( + dispatcherProvider = dispatcherProvider, + audioMessagePlayer = audioMessagePlayer, + ) + + @Provides + fun provideNetworkSettingsDefaultsProvider( + @ApplicationContext context: Context, + ): NetworkSettingsDefaultsProvider = AndroidNetworkSettingsDefaultsProvider(context) + + @Provides + fun provideCurrentSessionUseCase(@KaliumCoreLogic coreLogic: CoreLogic): CurrentSessionUseCase = + coreLogic.getGlobalScope().session.currentSession + + @Provides + fun provideRequestSecondFactorVerificationCodeUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): RequestSecondFactorVerificationCodeUseCase = + coreLogic.getSessionScope(currentAccount).authenticationScope.requestSecondFactorVerificationCode + + @Provides + fun provideObservePersistentWebSocketConnectionStatusUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ObservePersistentWebSocketConnectionStatusUseCase = + coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus + + @Provides + fun providePersistPersistentWebSocketConnectionStatusUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): PersistPersistentWebSocketConnectionStatusUseCase = + coreLogic.getSessionScope(currentAccount).persistPersistentWebSocketConnectionStatus + + @Provides + fun provideAboutThisAppInfoProvider( + @ApplicationContext context: Context, + ): AboutThisAppInfoProvider = AndroidAboutThisAppInfoProvider(context) + + @Provides + fun provideReleaseNotesFeedUrlProvider( + @ApplicationContext context: Context, + ): ReleaseNotesFeedUrlProvider = AndroidReleaseNotesFeedUrlProvider(context) + + @Provides + fun provideDependenciesInfoProvider( + @ApplicationContext context: Context, + ): DependenciesInfoProvider = AndroidDependenciesInfoProvider(context) + + @Provides + fun provideLicensesProvider( + @ApplicationContext context: Context, + ): LicensesProvider = AndroidLicensesProvider(context) +} + +fun createWireMetroGraph(context: Context): WireMetroGraph = + WireMetroGraphProvider.get(context.applicationContext) + +private object WireMetroGraphProvider { + @Volatile + private var graph: WireMetroGraph? = null + + fun get(applicationContext: Context): WireMetroGraph = + graph ?: synchronized(this) { + graph ?: createGraphFactory() + .create(applicationContext) + .also { graph = it } + } +} diff --git a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReporter.kt b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReporter.kt index 1e117e52295..4974bf6282d 100644 --- a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReporter.kt +++ b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReporter.kt @@ -20,7 +20,7 @@ package com.wire.android.emm import android.content.Context import androidx.enterprise.feedback.KeyedAppState import androidx.enterprise.feedback.KeyedAppStatesReporter -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt index 86f7342076f..d245897e583 100644 --- a/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt @@ -40,12 +40,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject import javax.inject.Singleton @Suppress("LongParameterList") @Singleton -class AccountSwitchUseCase @Inject constructor( +class AccountSwitchUseCase @Inject @MetroInject constructor( private val updateCurrentSession: UpdateCurrentSessionUseCase, private val getSessions: GetSessionsUseCase, private val getCurrentSession: CurrentSessionUseCase, diff --git a/app/src/main/kotlin/com/wire/android/feature/DisableAppLockUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/DisableAppLockUseCase.kt index 8f19a13a68a..ba1b3f27f27 100644 --- a/app/src/main/kotlin/com/wire/android/feature/DisableAppLockUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/DisableAppLockUseCase.kt @@ -19,12 +19,11 @@ package com.wire.android.feature import com.wire.android.datastore.GlobalDataStore import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase -import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.flow.firstOrNull +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject -@ViewModelScoped -class DisableAppLockUseCase @Inject constructor( +class DisableAppLockUseCase @Inject @MetroInject constructor( private val dataStore: GlobalDataStore, private val observeIsAppLockEditableUseCase: ObserveIsAppLockEditableUseCase ) { diff --git a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt index 19896940ebb..fd4c4af5c55 100644 --- a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt @@ -25,13 +25,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combineTransform +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @Singleton -class ObserveAppLockConfigUseCase @Inject constructor( +class ObserveAppLockConfigUseCase @Inject @MetroInject constructor( private val globalDataStore: GlobalDataStore, @KaliumCoreLogic private val coreLogic: CoreLogic ) { diff --git a/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt index 5d6ebdd8ba3..534cbb7a324 100644 --- a/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/ShouldStartPersistentWebSocketServiceUseCase.kt @@ -23,11 +23,12 @@ import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withTimeoutOrNull +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject import javax.inject.Singleton @Singleton -class ShouldStartPersistentWebSocketServiceUseCase @Inject constructor( +class ShouldStartPersistentWebSocketServiceUseCase @Inject @MetroInject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, private val managedConfigurationsManager: ManagedConfigurationsManager ) { diff --git a/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt index c1604192e11..d70e0b82000 100644 --- a/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt @@ -21,11 +21,12 @@ package com.wire.android.feature import com.wire.android.appLogger import com.wire.android.services.ServicesManager +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject import javax.inject.Singleton @Singleton -class StartPersistentWebsocketIfNecessaryUseCase @Inject constructor( +class StartPersistentWebsocketIfNecessaryUseCase @Inject @MetroInject constructor( private val servicesManager: ServicesManager, private val shouldStartPersistentWebSocketService: ShouldStartPersistentWebSocketServiceUseCase ) { diff --git a/app/src/main/kotlin/com/wire/android/media/CallRinger.kt b/app/src/main/kotlin/com/wire/android/media/CallRinger.kt index 71fd620ce18..d94eb6d29fc 100644 --- a/app/src/main/kotlin/com/wire/android/media/CallRinger.kt +++ b/app/src/main/kotlin/com/wire/android/media/CallRinger.kt @@ -28,11 +28,12 @@ import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager import com.wire.android.appLogger +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject import javax.inject.Singleton @Singleton -class CallRinger @Inject constructor(private val context: Context) { +class CallRinger @Inject @MetroInject constructor(private val context: Context) { private var mediaPlayer: MediaPlayer? = null private var vibrator: Vibrator? = null diff --git a/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioFocusHelper.kt b/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioFocusHelper.kt index ae7fef58222..4f0b2feea40 100644 --- a/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioFocusHelper.kt +++ b/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioFocusHelper.kt @@ -20,9 +20,10 @@ package com.wire.android.media.audiomessage import android.media.AudioFocusRequest import android.media.AudioManager import android.os.Build +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject -class AudioFocusHelper @Inject constructor(private val audioManager: AudioManager) { +class AudioFocusHelper @Inject @MetroInject constructor(private val audioManager: AudioManager) { private var listener: PlayPauseListener? = null diff --git a/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioMessageViewModel.kt b/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioMessageViewModel.kt index 9b28e66ba0b..08774c73580 100644 --- a/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioMessageViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioMessageViewModel.kt @@ -23,17 +23,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.di.AssistedViewModelFactory import com.wire.android.di.ScopedArgs import com.wire.android.di.ViewModelScopedPreview import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.AssetContent.AssetMetadata import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.feature.message.ObserveMessageByIdUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged @@ -51,11 +46,10 @@ interface AudioMessageViewModel { fun changeAudioSpeed(audioSpeed: AudioSpeed) {} } -@HiltViewModel(assistedFactory = AudioMessageViewModelImpl.Factory::class) -class AudioMessageViewModelImpl @AssistedInject constructor( +class AudioMessageViewModelImpl( private val audioMessagePlayer: ConversationAudioMessagePlayer, private val observeMessageById: ObserveMessageByIdUseCase, - @Assisted private val args: AudioMessageArgs, + private val args: AudioMessageArgs, ) : ViewModel(), AudioMessageViewModel { override var state: AudioMessageState by mutableStateOf(AudioMessageState()) @@ -139,11 +133,6 @@ class AudioMessageViewModelImpl @AssistedInject constructor( audioMessagePlayer.setSpeed(audioSpeed) } } - - @AssistedFactory - interface Factory : AssistedViewModelFactory { - override fun create(args: AudioMessageArgs): AudioMessageViewModelImpl - } } @Serializable diff --git a/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioMessageViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioMessageViewModelFactory.kt new file mode 100644 index 00000000000..ef96ec62612 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/media/audiomessage/AudioMessageViewModelFactory.kt @@ -0,0 +1,33 @@ +/* + * 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.media.audiomessage + +import com.wire.kalium.logic.feature.message.ObserveMessageByIdUseCase +import dev.zacsweers.metro.Inject + +@Inject +class AudioMessageViewModelFactory( + private val audioMessagePlayer: ConversationAudioMessagePlayer, + private val observeMessageById: ObserveMessageByIdUseCase, +) { + fun create(args: AudioMessageArgs): AudioMessageViewModelImpl = AudioMessageViewModelImpl( + audioMessagePlayer = audioMessagePlayer, + observeMessageById = observeMessageById, + args = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt b/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt index b74e10133b7..31c8eb92965 100644 --- a/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt +++ b/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt @@ -36,7 +36,7 @@ import com.wire.kalium.logic.feature.message.GetNextAudioMessageInConversationUs import com.wire.kalium.logic.feature.message.GetSenderNameByMessageIdUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import dagger.Lazy -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.channels.BufferOverflow @@ -58,13 +58,16 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject import javax.inject.Singleton @Singleton @Suppress("TooManyFunctions") class ConversationAudioMessagePlayer -@Inject constructor( +@Inject +@MetroInject +constructor( @ApplicationContext private val context: Context, private val audioMediaPlayer: MediaPlayer, private val servicesManager: Lazy, diff --git a/app/src/main/kotlin/com/wire/android/media/audiomessage/RecordAudioMessagePlayer.kt b/app/src/main/kotlin/com/wire/android/media/audiomessage/RecordAudioMessagePlayer.kt index 0d924538e5a..320d5a38c89 100644 --- a/app/src/main/kotlin/com/wire/android/media/audiomessage/RecordAudioMessagePlayer.kt +++ b/app/src/main/kotlin/com/wire/android/media/audiomessage/RecordAudioMessagePlayer.kt @@ -21,7 +21,6 @@ import android.content.Context import android.media.MediaPlayer import androidx.core.net.toUri import com.wire.android.di.ApplicationScope -import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.delay @@ -34,10 +33,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch import java.io.File +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject -@ViewModelScoped -class RecordAudioMessagePlayer @Inject constructor( +class RecordAudioMessagePlayer @Inject @MetroInject constructor( private val context: Context, private val audioMediaPlayer: MediaPlayer, private val audioFocusHelper: AudioFocusHelper, diff --git a/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt b/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt index dfe770da4f5..d8114b03852 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.compose.ui.platform.LocalContext import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.NewLoginPasswordScreenDestination @@ -47,7 +47,11 @@ import com.ramcosta.composedestinations.navigation.navGraph import com.ramcosta.composedestinations.scope.resultBackNavigator import com.ramcosta.composedestinations.scope.resultRecipient import com.ramcosta.composedestinations.spec.Direction +import com.wire.android.feature.cells.ui.CellFilesNavArgs import com.wire.android.feature.cells.ui.CellViewModel +import com.wire.android.di.metro.LocalMetroViewModelGraph +import com.wire.android.di.metro.createWireMetroGraph +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.sketch.model.DrawingCanvasNavBackArgs import com.wire.android.navigation.transition.LocalSharedTransitionScope import com.wire.android.ui.authentication.login.email.LoginEmailViewModel @@ -64,8 +68,13 @@ fun MainNavHost( modifier: Modifier = Modifier, ) { val navHostEngine = rememberWireNavHostEngine(Alignment.Center) + val context = LocalContext.current + val metroGraph = remember(context) { createWireMetroGraph(context) } SharedTransitionLayout(modifier = modifier) { - CompositionLocalProvider(LocalSharedTransitionScope provides this) { + CompositionLocalProvider( + LocalSharedTransitionScope provides this, + LocalMetroViewModelGraph provides metroGraph, + ) { DestinationsNavHost( modifier = Modifier, navGraph = WireRootGraph, @@ -86,7 +95,11 @@ fun MainNavHost( val parentEntry = remember(navBackStackEntry) { navController.getBackStackEntry(NewConversationGraph.route) } - dependency(hiltViewModel(parentEntry)) + dependency( + metroViewModel(parentEntry) { + newConversationViewModelFactory.create() + } + ) } // 👇 To reuse LoginEmailViewModel from NewLoginPasswordScreen on NewLoginVerificationCodeScreen @@ -94,7 +107,12 @@ fun MainNavHost( val loginPasswordEntry = remember(navBackStackEntry) { navController.getBackStackEntry(NewLoginPasswordScreenDestination.route) } - dependency(hiltViewModel(loginPasswordEntry)) + val args = NewLoginPasswordScreenDestination.argsFrom(loginPasswordEntry.arguments) + dependency( + metroViewModel(loginPasswordEntry) { + loginEmailViewModelFactory.create(args) + } + ) } // 👇 To reuse CellViewModel from the parent screen on SearchScreen @@ -102,7 +120,15 @@ fun MainNavHost( val parentEntry = remember(navBackStackEntry) { navController.previousBackStackEntry } - dependency(hiltViewModel(parentEntry ?: navBackStackEntry)) + dependency( + metroViewModel(parentEntry ?: navBackStackEntry) { + val searchArgs = SearchScreenDestination.argsFrom(navBackStackEntry) + cellViewModelFactory.create( + CellFilesNavArgs(conversationId = searchArgs.conversationId), + searchArgs + ) + } + ) } // 👇 To tie TeamMigrationViewModel to PersonalToTeamMigrationNavGraph, @@ -111,7 +137,11 @@ fun MainNavHost( val parentEntry = remember(navBackStackEntry) { navController.getBackStackEntry(PersonalToTeamMigrationGraph.route) } - dependency(hiltViewModel(parentEntry)) + dependency( + metroViewModel(parentEntry) { + teamMigrationViewModelFactory.create() + } + ) } }, manualComposableCallsBuilder = { @@ -120,8 +150,10 @@ fun MainNavHost( * those destinations to rely on generated dependencies directly. */ composable(ConversationScreenDestination) { + val args = ConversationScreenDestination.argsFrom(navBackStackEntry.arguments) ConversationScreen( navigator = navigator, + args = args, groupDetailsScreenResultRecipient = resultRecipient(groupConversationDetailsNavBackArgsNavType), mediaGalleryScreenResultRecipient = resultRecipient(mediaGalleryNavBackArgsNavType), imagePreviewScreenResultRecipient = resultRecipient(imagesPreviewNavBackArgsNavType), diff --git a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/BroadcastReceiverDependencies.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/BroadcastReceiverDependencies.kt new file mode 100644 index 00000000000..4b0ba1b7c0e --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/BroadcastReceiverDependencies.kt @@ -0,0 +1,93 @@ +/* + * 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.notification.broadcastreceivers + +import android.content.Context +import com.wire.android.config.NomadProfilesFeatureConfig +import com.wire.android.di.ApplicationScope +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.di.NoSession +import com.wire.android.di.metro.createWireMetroGraph +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase +import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer +import com.wire.android.notification.CallNotificationManager +import com.wire.android.util.SwitchAccountObserver +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import kotlinx.coroutines.CoroutineScope + +interface BroadcastReceiverDependencies { + @KaliumCoreLogic + fun coreLogic(): CoreLogic + + fun dispatcherProvider(): DispatcherProvider + + @NoSession + fun qualifiedIdMapper(): QualifiedIdMapper + + @ApplicationScope + fun coroutineScope(): CoroutineScope + + fun callNotificationManager(): CallNotificationManager + + fun conversationAudioMessagePlayer(): ConversationAudioMessagePlayer + + fun currentSession(): CurrentSessionUseCase + + fun accountSwitch(): AccountSwitchUseCase + + fun switchAccountObserver(): SwitchAccountObserver + + fun nomadProfilesFeatureConfig(): NomadProfilesFeatureConfig + + fun startPersistentWebsocketIfNecessary(): StartPersistentWebsocketIfNecessaryUseCase +} + +val Context.broadcastReceiverDependencies: BroadcastReceiverDependencies + get() { + val graph = createWireMetroGraph(applicationContext) + return object : BroadcastReceiverDependencies { + override fun coreLogic(): CoreLogic = graph.coreLogic + + override fun dispatcherProvider(): DispatcherProvider = graph.dispatcherProvider + + override fun qualifiedIdMapper(): QualifiedIdMapper = QualifiedIdMapper(null) + + override fun coroutineScope(): CoroutineScope = graph.applicationScope + + override fun callNotificationManager(): CallNotificationManager = graph.callNotificationManager + + override fun conversationAudioMessagePlayer(): ConversationAudioMessagePlayer = + graph.conversationAudioMessagePlayer + + override fun currentSession(): CurrentSessionUseCase = graph.currentSession + + override fun accountSwitch(): AccountSwitchUseCase = graph.accountSwitch + + override fun switchAccountObserver(): SwitchAccountObserver = graph.switchAccountObserver + + override fun nomadProfilesFeatureConfig(): NomadProfilesFeatureConfig = + graph.nomadProfilesFeatureConfig + + override fun startPersistentWebsocketIfNecessary(): StartPersistentWebsocketIfNecessaryUseCase = + graph.startPersistentWebsocketIfNecessary + } + } diff --git a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/DynamicReceiversManager.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/DynamicReceiversManager.kt index 455d3466d41..c7cfbacfade 100644 --- a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/DynamicReceiversManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/DynamicReceiversManager.kt @@ -23,17 +23,13 @@ import android.content.IntentFilter import com.wire.android.BuildConfig.EMM_SUPPORT_ENABLED import com.wire.android.appLogger import com.wire.android.emm.ManagedConfigurationsReceiver -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import javax.inject.Singleton /** * Manages dynamic registration and unregistration of broadcast receivers. * This are receivers that are active while the app is in foreground only. */ -@Singleton -class DynamicReceiversManager @Inject constructor( - @ApplicationContext val context: Context, +class DynamicReceiversManager( + private val context: Context, private val managedConfigurationsReceiver: ManagedConfigurationsReceiver ) { @Volatile diff --git a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/EndOngoingCallReceiver.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/EndOngoingCallReceiver.kt index fda113e96ad..f5405e023b2 100644 --- a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/EndOngoingCallReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/EndOngoingCallReceiver.kt @@ -22,43 +22,21 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.wire.android.appLogger -import com.wire.android.di.ApplicationScope -import com.wire.android.di.KaliumCoreLogic -import com.wire.android.di.NoSession -import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.id.QualifiedID -import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.toQualifiedID import com.wire.kalium.logic.feature.session.CurrentSessionResult -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -@AndroidEntryPoint class EndOngoingCallReceiver : BroadcastReceiver() { - @Inject - @KaliumCoreLogic - lateinit var coreLogic: CoreLogic - - @Inject - lateinit var dispatcherProvider: DispatcherProvider - - @Inject - @NoSession - lateinit var qualifiedIdMapper: QualifiedIdMapper - - @Inject - @ApplicationScope - lateinit var coroutineScope: CoroutineScope - override fun onReceive(context: Context, intent: Intent) { + val dependencies = context.broadcastReceiverDependencies + val coreLogic = dependencies.coreLogic() + val qualifiedIdMapper = dependencies.qualifiedIdMapper() val conversationId: String = intent.getStringExtra(EXTRA_CONVERSATION_ID) ?: return appLogger.i("EndOngoingCallReceiver: onReceive, conversationId: $conversationId") - coroutineScope.launch { + dependencies.coroutineScope().launch { val userId: QualifiedID? = intent.getStringExtra(EXTRA_RECEIVER_USER_ID)?.toQualifiedID(qualifiedIdMapper) val sessionScope = if (userId != null) { diff --git a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/IncomingCallActionReceiver.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/IncomingCallActionReceiver.kt index 64c4f312b85..98925d715d3 100644 --- a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/IncomingCallActionReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/IncomingCallActionReceiver.kt @@ -22,45 +22,18 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.wire.android.appLogger -import com.wire.android.di.ApplicationScope -import com.wire.android.di.KaliumCoreLogic -import com.wire.android.di.NoSession -import com.wire.android.notification.CallNotificationManager -import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logger.obfuscateId -import com.wire.kalium.logic.CoreLogic -import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.toQualifiedID import com.wire.kalium.logic.data.user.UserId -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import javax.inject.Inject -@AndroidEntryPoint class IncomingCallActionReceiver : BroadcastReceiver() { - @Inject - @KaliumCoreLogic - lateinit var coreLogic: CoreLogic - - @Inject - lateinit var dispatcherProvider: DispatcherProvider - - @Inject - @NoSession - lateinit var qualifiedIdMapper: QualifiedIdMapper - - @Inject - @ApplicationScope - lateinit var coroutineScope: CoroutineScope - - @Inject - lateinit var callNotificationManager: CallNotificationManager - @Suppress("ReturnCount") override fun onReceive(context: Context, intent: Intent) { + val dependencies = context.broadcastReceiverDependencies + val qualifiedIdMapper = dependencies.qualifiedIdMapper() val conversationIdString: String = intent.getStringExtra(EXTRA_CONVERSATION_ID) ?: run { appLogger.e("CallNotificationDismissReceiver: onReceive, conversation ID is missing") return @@ -75,14 +48,14 @@ class IncomingCallActionReceiver : BroadcastReceiver() { return } - coroutineScope.launch(Dispatchers.Default) { - with(coreLogic.getSessionScope(userId)) { + dependencies.coroutineScope().launch(Dispatchers.Default) { + with(dependencies.coreLogic().getSessionScope(userId)) { val conversationId = qualifiedIdMapper.fromStringToQualifiedID(conversationIdString) if (action == ACTION_DECLINE_CALL) { calls.rejectCall(conversationId) } } - callNotificationManager.hideIncomingCallNotification(userId.toString(), conversationIdString) + dependencies.callNotificationManager().hideIncomingCallNotification(userId.toString(), conversationIdString) } } diff --git a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiver.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiver.kt index 766156851dc..8e0757b7e21 100644 --- a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiver.kt @@ -20,49 +20,26 @@ package com.wire.android.notification.broadcastreceivers import android.content.Context import android.content.Intent import com.wire.android.appLogger -import com.wire.android.config.NomadProfilesFeatureConfig -import com.wire.android.di.KaliumCoreLogic -import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.SwitchAccountParam -import com.wire.android.util.SwitchAccountObserver import com.wire.android.util.lifecycle.AppBackgroundManager -import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.logout.LogoutReason import com.wire.kalium.logic.feature.session.CurrentSessionResult -import com.wire.kalium.logic.feature.session.CurrentSessionUseCase -import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import javax.inject.Inject -@AndroidEntryPoint class NomadLogoutReceiver : CoroutineReceiver() { - @Inject - @KaliumCoreLogic - lateinit var coreLogic: CoreLogic - - @Inject - lateinit var currentSession: CurrentSessionUseCase - - @Inject - lateinit var accountSwitch: AccountSwitchUseCase - - @Inject - lateinit var switchAccountObserver: SwitchAccountObserver - - @Inject - lateinit var nomadProfilesFeatureConfig: NomadProfilesFeatureConfig - public override suspend fun receive(context: Context, intent: Intent) { + val dependencies = context.broadcastReceiverDependencies + val coreLogic = dependencies.coreLogic() when { intent.action != ACTION_LOGOUT -> { appLogger.i("$TAG not a logout intent is passed ignore") } - !nomadProfilesFeatureConfig.isEnabled() -> { + !dependencies.nomadProfilesFeatureConfig().isEnabled() -> { appLogger.i("$TAG nomadProfilesFeatureConfig is not enabled ignoring") } @@ -75,7 +52,7 @@ class NomadLogoutReceiver : CoroutineReceiver() { @Suppress("TooGenericExceptionCaught") try { - performLogout() + performLogout(dependencies) CoroutineScope(Dispatchers.Default).launch { AppBackgroundManager.moveAppToBackground() } @@ -88,14 +65,16 @@ class NomadLogoutReceiver : CoroutineReceiver() { } } - private suspend fun performLogout() { - when (val session = currentSession()) { + private suspend fun performLogout(dependencies: BroadcastReceiverDependencies) { + val coreLogic = dependencies.coreLogic() + when (val session = dependencies.currentSession()()) { is CurrentSessionResult.Success -> { val userId = session.accountInfo.userId appLogger.i("$TAG Logging out user: ${userId.toLogString()}") coreLogic.getSessionScope(userId).logout(LogoutReason.SELF_HARD_LOGOUT, waitUntilCompletes = true) coreLogic.getGlobalScope().deleteSession(userId) - accountSwitch(SwitchAccountParam.TryToSwitchToNextAccount).callAction(switchAccountObserver) + dependencies.accountSwitch()(SwitchAccountParam.TryToSwitchToNextAccount) + .callAction(dependencies.switchAccountObserver()) } is CurrentSessionResult.Failure.SessionNotFound -> diff --git a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NotificationReplyReceiver.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NotificationReplyReceiver.kt index 65d0a8dae26..77ae7c6a47b 100644 --- a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NotificationReplyReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/NotificationReplyReceiver.kt @@ -23,34 +23,17 @@ import android.content.Intent import android.widget.Toast import androidx.core.app.RemoteInput import com.wire.android.R -import com.wire.android.di.KaliumCoreLogic -import com.wire.android.di.NoSession import com.wire.android.notification.MessageNotificationManager import com.wire.android.notification.NotificationConstants -import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.common.functional.fold -import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.id.QualifiedID -import com.wire.kalium.logic.data.id.QualifiedIdMapper -import dagger.hilt.android.AndroidEntryPoint import kotlinx.datetime.Clock -import javax.inject.Inject -@AndroidEntryPoint class NotificationReplyReceiver : CoroutineReceiver() { // requires zero argument constructor - @Inject - @KaliumCoreLogic - lateinit var coreLogic: CoreLogic - - @Inject - lateinit var dispatcherProvider: DispatcherProvider - - @Inject - @NoSession - lateinit var qualifiedIdMapper: QualifiedIdMapper - override suspend fun receive(context: Context, intent: Intent) { + val dependencies = context.broadcastReceiverDependencies + val qualifiedIdMapper = dependencies.qualifiedIdMapper() val remoteInput = RemoteInput.getResultsFromIntent(intent) val conversationId: String? = intent.getStringExtra(EXTRA_CONVERSATION_ID) val userId: String? = intent.getStringExtra(EXTRA_USER_ID) @@ -60,7 +43,7 @@ class NotificationReplyReceiver : CoroutineReceiver() { // requires zero argumen val qualifiedUserId = qualifiedIdMapper.fromStringToQualifiedID(userId) val qualifiedConversationId = qualifiedIdMapper.fromStringToQualifiedID(conversationId) - with(coreLogic.getSessionScope(qualifiedUserId)) { + with(dependencies.coreLogic().getSessionScope(qualifiedUserId)) { syncExecutor.request { messages.sendTextMessage(qualifiedConversationId, replyText).toEither() .fold( @@ -84,6 +67,7 @@ class NotificationReplyReceiver : CoroutineReceiver() { // requires zero argumen val userId: String? = intent.getStringExtra(EXTRA_USER_ID) if (conversationId != null && userId != null) { + val qualifiedIdMapper = context.broadcastReceiverDependencies.qualifiedIdMapper() val qualifiedUserId = qualifiedIdMapper.fromStringToQualifiedID(userId) updateNotification(context, conversationId, qualifiedUserId, null) } diff --git a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/PlayPauseAudioMessageReceiver.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/PlayPauseAudioMessageReceiver.kt index 9cb6998f403..1e5cc59d1fe 100644 --- a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/PlayPauseAudioMessageReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/PlayPauseAudioMessageReceiver.kt @@ -22,28 +22,16 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.wire.android.appLogger -import com.wire.android.di.ApplicationScope -import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -@AndroidEntryPoint class PlayPauseAudioMessageReceiver : BroadcastReceiver() { - @Inject - lateinit var audioMessagePlayer: ConversationAudioMessagePlayer - - @Inject - @ApplicationScope - lateinit var coroutineScope: CoroutineScope - override fun onReceive(context: Context, intent: Intent) { appLogger.i("PlayPauseAudioMessageReceiver: onReceive") - coroutineScope.launch { - audioMessagePlayer.resumeOrPauseCurrentAudioMessage() + val dependencies = context.broadcastReceiverDependencies + dependencies.coroutineScope().launch { + dependencies.conversationAudioMessagePlayer().resumeOrPauseCurrentAudioMessage() } } diff --git a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/StopAudioMessageReceiver.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/StopAudioMessageReceiver.kt index 8ffe59f64ea..a0699b0e1bb 100644 --- a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/StopAudioMessageReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/StopAudioMessageReceiver.kt @@ -22,27 +22,15 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.wire.android.appLogger -import com.wire.android.di.ApplicationScope -import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import javax.inject.Inject -@AndroidEntryPoint class StopAudioMessageReceiver : BroadcastReceiver() { - @Inject - lateinit var audioMessagePlayer: ConversationAudioMessagePlayer - - @Inject - @ApplicationScope - lateinit var coroutineScope: CoroutineScope - override fun onReceive(context: Context, intent: Intent) { appLogger.i("StopAudioMessageReceiver: onReceive") - coroutineScope.launch { - audioMessagePlayer.forceToStopCurrentAudioMessage() + val dependencies = context.broadcastReceiverDependencies + dependencies.coroutineScope().launch { + dependencies.conversationAudioMessagePlayer().forceToStopCurrentAudioMessage() } } diff --git a/app/src/main/kotlin/com/wire/android/services/CallService.kt b/app/src/main/kotlin/com/wire/android/services/CallService.kt index 4fe78858383..e92ea9789af 100644 --- a/app/src/main/kotlin/com/wire/android/services/CallService.kt +++ b/app/src/main/kotlin/com/wire/android/services/CallService.kt @@ -26,6 +26,7 @@ import android.content.pm.ServiceInfo import android.os.IBinder import androidx.core.app.ServiceCompat import com.wire.android.appLogger +import com.wire.android.di.metro.createWireMetroGraph import com.wire.android.notification.CallNotificationData import com.wire.android.notification.CallNotificationManager import com.wire.android.notification.NotificationIds @@ -35,7 +36,6 @@ import com.wire.kalium.common.functional.fold import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId -import dagger.hilt.android.AndroidEntryPoint import dev.ahmedmourad.bundlizer.Bundlizer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -45,21 +45,16 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import javax.inject.Inject /** * Service that will be started when we have an outgoing/established call. */ -@AndroidEntryPoint class CallService : Service() { - @Inject lateinit var lifecycleManager: CallServiceManager - @Inject lateinit var callNotificationManager: CallNotificationManager - @Inject lateinit var dispatcherProvider: DispatcherProvider private val scope by lazy { @@ -68,11 +63,19 @@ class CallService : Service() { } override fun onCreate() { - _serviceState.value = ServiceState.STARTED super.onCreate() + injectDependencies() + _serviceState.value = ServiceState.STARTED handleActions() } + private fun injectDependencies() { + val dependencies = createWireMetroGraph(this).callServiceDependencies + lifecycleManager = dependencies.lifecycleManager + callNotificationManager = dependencies.callNotificationManager + dispatcherProvider = dependencies.dispatcherProvider + } + override fun onBind(intent: Intent?): IBinder? { return null } @@ -152,6 +155,12 @@ class CallService : Service() { NOT_STARTED, STARTED, FOREGROUND } + data class Dependencies( + val lifecycleManager: CallServiceManager, + val callNotificationManager: CallNotificationManager, + val dispatcherProvider: DispatcherProvider + ) + @Serializable sealed interface Action { @Serializable diff --git a/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt b/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt index 02d055da302..750215e35bf 100644 --- a/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt +++ b/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt @@ -31,6 +31,7 @@ import androidx.core.app.ServiceCompat import com.wire.android.R import com.wire.android.appLogger import com.wire.android.di.KaliumCoreLogic +import com.wire.android.di.metro.createWireMetroGraph import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.NotificationConstants.WEB_SOCKET_CHANNEL_ID import com.wire.android.notification.NotificationConstants.WEB_SOCKET_CHANNEL_NAME @@ -41,7 +42,6 @@ import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase -import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.awaitCancellation @@ -49,26 +49,20 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import javax.inject.Inject -@AndroidEntryPoint class PersistentWebSocketService : Service() { - @Inject @KaliumCoreLogic lateinit var coreLogic: CoreLogic - @Inject lateinit var dispatcherProvider: DispatcherProvider private val scope by lazy { CoroutineScope(SupervisorJob() + dispatcherProvider.io()) } - @Inject lateinit var notificationManager: WireNotificationManager - @Inject lateinit var notificationChannelsManager: NotificationChannelsManager override fun onBind(intent: Intent?): IBinder? { @@ -77,10 +71,19 @@ class PersistentWebSocketService : Service() { override fun onCreate() { super.onCreate() + injectDependencies() isServiceStarted = true generateForegroundNotification() } + private fun injectDependencies() { + val dependencies = createWireMetroGraph(this).persistentWebSocketServiceDependencies + coreLogic = dependencies.coreLogic + dispatcherProvider = dependencies.dispatcherProvider + notificationManager = dependencies.notificationManager + notificationChannelsManager = dependencies.notificationChannelsManager + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { /** * When service is restarted by system onCreate lifecycle method is not guaranteed to be called @@ -178,4 +181,11 @@ class PersistentWebSocketService : Service() { var isServiceStarted = false } + + data class Dependencies( + @KaliumCoreLogic val coreLogic: CoreLogic, + val dispatcherProvider: DispatcherProvider, + val notificationManager: WireNotificationManager, + val notificationChannelsManager: NotificationChannelsManager + ) } diff --git a/app/src/main/kotlin/com/wire/android/services/PlayingAudioMessageService.kt b/app/src/main/kotlin/com/wire/android/services/PlayingAudioMessageService.kt index 83312d2269e..3a80ff1dffe 100644 --- a/app/src/main/kotlin/com/wire/android/services/PlayingAudioMessageService.kt +++ b/app/src/main/kotlin/com/wire/android/services/PlayingAudioMessageService.kt @@ -33,6 +33,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat import com.wire.android.R import com.wire.android.appLogger +import com.wire.android.di.metro.createWireMetroGraph import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer import com.wire.android.media.audiomessage.PlayingAudioMessage import com.wire.android.notification.NotificationConstants.PLAYING_AUDIO_CHANNEL_ID @@ -41,26 +42,21 @@ import com.wire.android.notification.openAppPendingIntent import com.wire.android.notification.playPauseAudioPendingIntent import com.wire.android.notification.stopAudioPendingIntent import com.wire.android.util.dispatchers.DispatcherProvider -import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import javax.inject.Inject -@AndroidEntryPoint class PlayingAudioMessageService : Service() { - @Inject lateinit var dispatcherProvider: DispatcherProvider private val scope by lazy { CoroutineScope(SupervisorJob() + dispatcherProvider.io()) } - @Inject lateinit var audioMessagePlayer: ConversationAudioMessagePlayer override fun onBind(intent: Intent?): IBinder? { @@ -69,11 +65,18 @@ class PlayingAudioMessageService : Service() { override fun onCreate() { super.onCreate() + injectDependencies() appLogger.i("$TAG: starting foreground") isServiceStarted = true generateForegroundNotification(null) } + private fun injectDependencies() { + val dependencies = createWireMetroGraph(this).playingAudioMessageServiceDependencies + dispatcherProvider = dependencies.dispatcherProvider + audioMessagePlayer = dependencies.audioMessagePlayer + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { /** * When service is restarted by system onCreate lifecycle method is not guaranteed to be called @@ -170,4 +173,9 @@ class PlayingAudioMessageService : Service() { var isServiceStarted = false } + + data class Dependencies( + val dispatcherProvider: DispatcherProvider, + val audioMessagePlayer: ConversationAudioMessagePlayer + ) } diff --git a/app/src/main/kotlin/com/wire/android/services/ServicesManager.kt b/app/src/main/kotlin/com/wire/android/services/ServicesManager.kt index 254b959b60a..c2e25785a87 100644 --- a/app/src/main/kotlin/com/wire/android/services/ServicesManager.kt +++ b/app/src/main/kotlin/com/wire/android/services/ServicesManager.kt @@ -26,6 +26,7 @@ import com.wire.android.appLogger import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId +import com.wire.android.di.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableSharedFlow @@ -43,7 +44,7 @@ import javax.inject.Singleton */ @Singleton class ServicesManager @Inject constructor( - private val context: Context, + @ApplicationContext private val context: Context, dispatcherProvider: DispatcherProvider, ) { private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.default()) diff --git a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt index fc54baf8268..e12918ce6f4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt @@ -28,20 +28,21 @@ import com.ramcosta.composedestinations.generated.app.destinations.AppUnlockWith import com.ramcosta.composedestinations.generated.app.destinations.EnterLockCodeScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.SetLockCodeScreenDestination import com.wire.android.appLogger -import com.wire.android.navigation.LoginTypeSelector +import com.wire.android.di.metro.createWireMetroGraph import com.wire.android.navigation.MainNavHost import com.wire.android.navigation.rememberNavigator import com.wire.android.ui.common.setupOrientationForDevice import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.WireTheme -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject -@AndroidEntryPoint class AppLockActivity : BaseActivity() { - @Inject - lateinit var loginTypeSelector: LoginTypeSelector + private val metroGraph by lazy(LazyThreadSafetyMode.NONE) { + createWireMetroGraph(this) + } + private val loginTypeSelector by lazy(LazyThreadSafetyMode.NONE) { + metroGraph.loginTypeSelector + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt index 9ab12dbfcf6..3fc126cf05c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt @@ -33,7 +33,6 @@ import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCaseResult import dagger.Lazy -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged @@ -42,10 +41,8 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CallFeedbackViewModel @Inject constructor( +class CallFeedbackViewModel( @KaliumCoreLogic private val coreLogic: Lazy, private val currentSessionFlow: Lazy, private val isAnalyticsAvailable: Lazy, diff --git a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModelFactory.kt new file mode 100644 index 00000000000..ff64bb058c1 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModelFactory.kt @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui + +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.ui.analytics.IsAnalyticsAvailableUseCase +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase +import dagger.Lazy +import dev.zacsweers.metro.Inject + +@Inject +class CallFeedbackViewModelFactory( + @KaliumCoreLogic private val coreLogic: Lazy, + private val currentSessionFlow: Lazy, + private val isAnalyticsAvailable: Lazy, + private val analyticsManager: Lazy, +) { + fun create(): CallFeedbackViewModel = CallFeedbackViewModel( + coreLogic = coreLogic, + currentSessionFlow = currentSessionFlow, + isAnalyticsAvailable = isAnalyticsAvailable, + analyticsManager = analyticsManager, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 2bd547acbbb..ebfed5f931a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -24,7 +24,6 @@ import android.os.Bundle import android.view.WindowManager import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.layout.Column @@ -49,8 +48,12 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory import androidx.navigation.NavController import androidx.navigation.compose.currentBackStackEntryAsState import com.ramcosta.composedestinations.generated.app.destinations.E2EIEnrollmentScreenDestination @@ -72,11 +75,10 @@ import com.wire.android.appLogger import com.wire.android.config.CustomUiConfigurationProvider import com.wire.android.config.LocalCustomUiConfigurationProvider import com.wire.android.datastore.UserDataStore -import com.wire.android.di.assistedViewModels -import com.wire.android.emm.ManagedConfigurationsManager +import com.wire.android.di.metro.WireMetroGraph +import com.wire.android.di.metro.createWireMetroGraph import com.wire.android.feature.NavigationSwitchAccountActions import com.wire.android.navigation.BackStackMode -import com.wire.android.navigation.LoginTypeSelector import com.wire.android.navigation.MainNavHost import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -86,7 +88,6 @@ import com.wire.android.navigation.rememberNavigator import com.wire.android.navigation.startDestination import com.wire.android.navigation.style.BackgroundStyle import com.wire.android.navigation.style.BackgroundType -import com.wire.android.notification.broadcastreceivers.DynamicReceiversManager import com.wire.android.ui.authentication.login.WireAuthBackgroundLayout import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.bottomsheet.show @@ -102,7 +103,6 @@ import com.wire.android.ui.home.E2EIRequiredDialog import com.wire.android.ui.home.E2EIResultDialog import com.wire.android.ui.home.E2EISnoozeDialog import com.wire.android.ui.home.FeatureFlagState -import com.wire.android.ui.home.appLock.LockCodeTimeManager import com.wire.android.ui.home.sync.FeatureFlagNotificationViewModel import com.wire.android.ui.legalhold.dialog.deactivated.LegalHoldDeactivatedDialog import com.wire.android.ui.legalhold.dialog.deactivated.LegalHoldDeactivatedState @@ -115,16 +115,12 @@ import com.wire.android.ui.theme.ThemeOption import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialog import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialogState -import com.wire.android.util.CurrentScreenManager import com.wire.android.util.LocalSyncStateObserver import com.wire.android.util.ShakeDetector -import com.wire.android.util.SwitchAccountObserver import com.wire.android.util.SyncStateObserver import com.wire.android.util.debug.FeatureVisibilityFlags import com.wire.android.util.debug.LocalFeatureVisibilityFlags import com.wire.android.util.launchUpdateTheApp -import dagger.Lazy -import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collectLatest @@ -132,40 +128,42 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject @OptIn(ExperimentalComposeUiApi::class) -@AndroidEntryPoint @Suppress("TooManyFunctions", "LargeClass") class WireActivity : BaseActivity() { - @Inject - lateinit var currentScreenManager: CurrentScreenManager - - @Inject - lateinit var lockCodeTimeManager: Lazy - - @Inject - lateinit var switchAccountObserver: SwitchAccountObserver - - @Inject - lateinit var loginTypeSelector: LoginTypeSelector - - @Inject - lateinit var dynamicReceiversManager: DynamicReceiversManager - - @Inject - lateinit var managedConfigurationsManager: ManagedConfigurationsManager - - private val viewModel: WireActivityViewModel by viewModels() - private val featureFlagNotificationViewModel: FeatureFlagNotificationViewModel by viewModels() - private val callFeedbackViewModel: CallFeedbackViewModel by viewModels() + private val metroGraph by lazy(LazyThreadSafetyMode.NONE) { + createWireMetroGraph(this) + } + private val currentScreenManager by lazy(LazyThreadSafetyMode.NONE) { metroGraph.currentScreenManager } + private val lockCodeTimeManager by lazy(LazyThreadSafetyMode.NONE) { metroGraph.lockCodeTimeManager } + private val switchAccountObserver by lazy(LazyThreadSafetyMode.NONE) { metroGraph.switchAccountObserver } + private val loginTypeSelector by lazy(LazyThreadSafetyMode.NONE) { metroGraph.loginTypeSelector } + private val dynamicReceiversManager by lazy(LazyThreadSafetyMode.NONE) { metroGraph.dynamicReceiversManager } + private val managedConfigurationsManager by lazy(LazyThreadSafetyMode.NONE) { metroGraph.managedConfigurationsManager } + + private val viewModel: WireActivityViewModel by metroActivityViewModel { + wireActivityViewModelFactory.create() + } + private val featureFlagNotificationViewModel: FeatureFlagNotificationViewModel by metroActivityViewModel { + featureFlagNotificationViewModelFactory.create() + } + private val callFeedbackViewModel: CallFeedbackViewModel by metroActivityViewModel { + callFeedbackViewModelFactory.create() + } - private val commonTopAppBarViewModel by assistedViewModels { factory -> - factory.create(CommonTopAppBarParams(showNoNetwork = true, showSync = true, showActiveCalls = true)) + private val commonTopAppBarViewModel: CommonTopAppBarViewModel by metroActivityViewModel { + commonTopAppBarViewModelFactory.create( + CommonTopAppBarParams(showNoNetwork = true, showSync = true, showActiveCalls = true) + ) + } + private val legalHoldRequestedViewModel: LegalHoldRequestedViewModel by metroActivityViewModel { + legalHoldRequestedViewModelFactory.create() + } + private val legalHoldDeactivatedViewModel: LegalHoldDeactivatedViewModel by metroActivityViewModel { + legalHoldDeactivatedViewModelFactory.create() } - private val legalHoldRequestedViewModel: LegalHoldRequestedViewModel by viewModels() - private val legalHoldDeactivatedViewModel: LegalHoldDeactivatedViewModel by viewModels() private val newIntents = Channel>(Channel.UNLIMITED) // keep new intents until subscribed but do not replay them private lateinit var shakeDetector: ShakeDetector @@ -614,7 +612,7 @@ class WireActivity : BaseActivity() { shakeDetector.start() lifecycleScope.launch { - lockCodeTimeManager.get().observeAppLock() + lockCodeTimeManager.observeAppLock() // Listen to one flow in a lifecycle-aware manner using flowWithLifecycle .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .first().let { @@ -681,16 +679,16 @@ class WireActivity : BaseActivity() { || originalIntent == intent // This is the case when the activity is recreated and already handled || intent.getBooleanExtra(HANDLED_DEEPLINK_FLAG, false) ) { - val handled = viewModel.handleIntentsThatAreNotDeepLinks(intent) + val handled = viewModel.handleIntentsThatAreNotDeepLinks(intent?.toWireActivityIntentContent()) if (!handled && navigator.isEmptyWelcomeStartDestination()) { // nothing to handle so if "welcome empty start" screen then switch "start" screen to login by navigating to it navigate(NavigationCommand(NewLoginScreenDestination(), BackStackMode.CLEAR_WHOLE)) } return } else { - val handled = viewModel.handleIntentsThatAreNotDeepLinks(intent) + val handled = viewModel.handleIntentsThatAreNotDeepLinks(intent.toWireActivityIntentContent()) if (!handled) { - viewModel.handleDeepLink(intent) + viewModel.handleDeepLink(intent.toWireActivityIntentContent()) intent.putExtra(HANDLED_DEEPLINK_FLAG, true) } } @@ -724,6 +722,15 @@ class WireActivity : BaseActivity() { } } + private inline fun metroActivityViewModel( + crossinline create: WireMetroGraph.() -> VM, + ): kotlin.Lazy = lazy(LazyThreadSafetyMode.NONE) { + val factory = viewModelFactory { + initializer { metroGraph.create() } + } + ViewModelProvider(this, factory)[VM::class.java] + } + companion object { private const val HANDLED_DEEPLINK_FLAG = "deeplink_handled_flag_key" private const val ORIGINAL_SAVED_INTENT_FLAG = "original_saved_intent" diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityIntentGateway.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityIntentGateway.kt new file mode 100644 index 00000000000..db0c85e0577 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityIntentGateway.kt @@ -0,0 +1,56 @@ +/* + * 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 + +import android.content.Intent +import androidx.core.net.toUri +import com.wire.android.util.deeplink.DeepLinkProcessor +import com.wire.android.util.deeplink.DeepLinkResult +import com.wire.android.util.lifecycle.AutomatedLoginViaSSO +import com.wire.android.util.lifecycle.IntentsProcessor +import dagger.Lazy + +data class WireActivityIntentContent( + val dataUri: String?, + val action: String?, + val automatedLogin: String?, +) + +interface WireActivityIntentGateway { + suspend fun parseDeepLink(intentContent: WireActivityIntentContent?): DeepLinkResult + suspend fun parseAutomatedLogin(intentContent: WireActivityIntentContent?): AutomatedLoginViaSSO? +} + +class AndroidWireActivityIntentGateway( + private val deepLinkProcessor: Lazy, + private val intentsProcessor: Lazy, +) : WireActivityIntentGateway { + + override suspend fun parseDeepLink(intentContent: WireActivityIntentContent?): DeepLinkResult = + deepLinkProcessor.get().invoke(intentContent?.dataUri?.toUri(), intentContent?.action) + + override suspend fun parseAutomatedLogin(intentContent: WireActivityIntentContent?): AutomatedLoginViaSSO? = + intentsProcessor.get().parseAutomatedLogin(intentContent?.automatedLogin) +} + +fun Intent.toWireActivityIntentContent(): WireActivityIntentContent = + WireActivityIntentContent( + dataUri = data?.toString(), + action = action, + automatedLogin = getStringExtra(IntentsProcessor.AUTOMATED_LOGIN), + ) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 52779d1e34b..ab16f07106b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -18,7 +18,6 @@ package com.wire.android.ui -import android.content.Intent import androidx.annotation.VisibleForTesting import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -54,12 +53,10 @@ import com.wire.android.ui.theme.Accent import com.wire.android.ui.theme.ThemeOption import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager -import com.wire.android.util.deeplink.DeepLinkProcessor import com.wire.android.util.deeplink.DeepLinkResult import com.wire.android.util.deeplink.LoginType import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.lifecycle.AutomatedLoginManager -import com.wire.android.util.lifecycle.IntentsProcessor import com.wire.android.util.ui.UIText import com.wire.android.workmanager.worker.cancelPeriodicPersistentWebsocketCheckWorker import com.wire.android.workmanager.worker.enqueuePeriodicPersistentWebsocketCheckWorker @@ -92,7 +89,6 @@ import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotC import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString import dagger.Lazy -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -112,21 +108,18 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.InputStream import java.io.InputStreamReader -import javax.inject.Inject private const val AUTOMATED_NOMAD_COOKIE_LABEL = "shared-device" @Suppress("LongParameterList", "TooManyFunctions") @OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel -class WireActivityViewModel @Inject constructor( +class WireActivityViewModel( @KaliumCoreLogic private val coreLogic: Lazy, private val dispatchers: DispatcherProvider, currentSessionFlow: Lazy, private val doesValidSessionExist: Lazy, private val getServerConfigUseCase: Lazy, - private val deepLinkProcessor: Lazy, - private val intentsProcessor: Lazy, + private val intentGateway: Lazy, private val observeSessions: Lazy, private val accountSwitch: Lazy, private val servicesManager: Lazy, @@ -360,9 +353,9 @@ class WireActivityViewModel @Inject constructor( } @Suppress("ComplexMethod") - fun handleDeepLink(intent: Intent?) { + fun handleDeepLink(intentContent: WireActivityIntentContent?) { viewModelScope.launch(dispatchers.io()) { - when (val result = deepLinkProcessor.get().invoke(intent?.data, intent?.action)) { + when (val result = intentGateway.get().parseDeepLink(intentContent)) { DeepLinkResult.AuthorizationNeeded -> sendAction(OnAuthorizationNeeded) is DeepLinkResult.SSOLogin -> sendAction(OnSSOLogin(result)) is DeepLinkResult.CustomServerConfig -> onCustomServerConfig(result.url, result.loginType) @@ -391,8 +384,8 @@ class WireActivityViewModel @Inject constructor( // Returns whether an intent was handled, or if there was nothing to do @Suppress("ReturnCount") - suspend fun handleIntentsThatAreNotDeepLinks(intent: Intent?): Boolean { - val result = intentsProcessor.get().invoke(intent) + suspend fun handleIntentsThatAreNotDeepLinks(intentContent: WireActivityIntentContent?): Boolean { + val result = intentGateway.get().parseAutomatedLogin(intentContent) if (result != null) { if (!nomadProfilesFeatureConfig.isEnabled()) { appLogger.w("Nomad login ignored: local Nomad profiles flag is disabled") diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt new file mode 100644 index 00000000000..e1bb7d981c2 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt @@ -0,0 +1,109 @@ +/* + * 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 + +import androidx.work.WorkManager +import com.wire.android.config.NomadProfilesFeatureConfig +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.di.IsProfileQRCodeEnabledUseCaseProvider +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.di.ObserveIfE2EIRequiredDuringLoginUseCaseProvider +import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider +import com.wire.android.di.ObserveSelfUserUseCaseProvider +import com.wire.android.di.ObserveSyncStateUseCaseProvider +import com.wire.android.emm.ManagedConfigurationsManager +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.navigation.LoginTypeSelector +import com.wire.android.services.ServicesManager +import com.wire.android.sync.MonitorSyncWorkUseCase +import com.wire.android.util.CurrentScreenManager +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.android.util.lifecycle.AutomatedLoginManager +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.feature.appVersioning.ObserveIfAppUpdateRequiredUseCase +import com.wire.kalium.logic.feature.client.ClearNewClientsForUserUseCase +import com.wire.kalium.logic.feature.client.ObserveNewClientsUseCase +import com.wire.kalium.logic.feature.server.GetServerConfigUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase +import com.wire.kalium.logic.feature.session.DoesValidNomadAccountExistUseCase +import com.wire.kalium.logic.feature.session.DoesValidSessionExistUseCase +import com.wire.kalium.logic.feature.session.ObserveSessionsUseCase +import dagger.Lazy +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class WireActivityViewModelFactory( + @KaliumCoreLogic private val coreLogic: Lazy, + private val dispatchers: DispatcherProvider, + private val currentSessionFlow: Lazy, + private val doesValidSessionExist: Lazy, + private val getServerConfigUseCase: Lazy, + private val intentGateway: Lazy, + private val observeSessions: Lazy, + private val accountSwitch: Lazy, + private val servicesManager: Lazy, + private val observeSyncStateUseCaseProviderFactory: ObserveSyncStateUseCaseProvider.Factory, + private val observeIfAppUpdateRequired: Lazy, + private val observeNewClients: Lazy, + private val clearNewClientsForUser: Lazy, + private val currentScreenManager: Lazy, + private val observeScreenshotCensoringConfigUseCaseProviderFactory: + ObserveScreenshotCensoringConfigUseCaseProvider.Factory, + private val globalDataStore: Lazy, + private val observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: + ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory, + private val workManager: Lazy, + private val isProfileQRCodeEnabledFactory: IsProfileQRCodeEnabledUseCaseProvider.Factory, + private val observeSelfUserFactory: ObserveSelfUserUseCaseProvider.Factory, + private val monitorSyncWorkUseCase: MonitorSyncWorkUseCase, + private val managedConfigurationsManager: ManagedConfigurationsManager, + private val automatedLoginManager: AutomatedLoginManager, + private val nomadProfilesFeatureConfig: NomadProfilesFeatureConfig, + private val loginTypeSelector: LoginTypeSelector, + private val doesValidNomadAccountExist: Lazy, +) { + fun create(): WireActivityViewModel = WireActivityViewModel( + coreLogic = coreLogic, + dispatchers = dispatchers, + currentSessionFlow = currentSessionFlow, + doesValidSessionExist = doesValidSessionExist, + getServerConfigUseCase = getServerConfigUseCase, + intentGateway = intentGateway, + observeSessions = observeSessions, + accountSwitch = accountSwitch, + servicesManager = servicesManager, + observeSyncStateUseCaseProviderFactory = observeSyncStateUseCaseProviderFactory, + observeIfAppUpdateRequired = observeIfAppUpdateRequired, + observeNewClients = observeNewClients, + clearNewClientsForUser = clearNewClientsForUser, + currentScreenManager = currentScreenManager, + observeScreenshotCensoringConfigUseCaseProviderFactory = observeScreenshotCensoringConfigUseCaseProviderFactory, + globalDataStore = globalDataStore, + observeIfE2EIRequiredDuringLoginUseCaseProviderFactory = observeIfE2EIRequiredDuringLoginUseCaseProviderFactory, + workManager = workManager, + isProfileQRCodeEnabledFactory = isProfileQRCodeEnabledFactory, + observeSelfUserFactory = observeSelfUserFactory, + monitorSyncWorkUseCase = monitorSyncWorkUseCase, + managedConfigurationsManager = managedConfigurationsManager, + automatedLoginManager = automatedLoginManager, + nomadProfilesFeatureConfig = nomadProfilesFeatureConfig, + loginTypeSelector = loginTypeSelector, + doesValidNomadAccountExist = doesValidNomadAccountExist, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/WireTestActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireTestActivity.kt index 324a1c5c4b4..e8e43337da2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireTestActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireTestActivity.kt @@ -18,7 +18,5 @@ package com.wire.android.ui import androidx.activity.ComponentActivity -import dagger.hilt.android.AndroidEntryPoint -@AndroidEntryPoint class WireTestActivity : ComponentActivity() diff --git a/app/src/main/kotlin/com/wire/android/ui/analytics/AnalyticsUsageViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/analytics/AnalyticsUsageViewModel.kt index f8fe47b6f0d..68f051a4f96 100644 --- a/app/src/main/kotlin/com/wire/android/ui/analytics/AnalyticsUsageViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/analytics/AnalyticsUsageViewModel.kt @@ -26,13 +26,10 @@ import com.wire.android.datastore.UserDataStore import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase import dagger.Lazy -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class AnalyticsUsageViewModel @Inject constructor( +class AnalyticsUsageViewModel( private val analyticsEnabled: AnalyticsConfiguration, private val dataStore: Lazy, private val selfServerConfig: Lazy, diff --git a/app/src/main/kotlin/com/wire/android/ui/analytics/AnalyticsUsageViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/analytics/AnalyticsUsageViewModelFactory.kt new file mode 100644 index 00000000000..88cabd1614a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/analytics/AnalyticsUsageViewModelFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.analytics + +import com.wire.android.datastore.UserDataStore +import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase +import dagger.Lazy +import dev.zacsweers.metro.Inject + +@Inject +class AnalyticsUsageViewModelFactory( + private val analyticsEnabled: AnalyticsConfiguration, + private val dataStore: Lazy, + private val selfServerConfig: Lazy, +) { + fun create(): AnalyticsUsageViewModel = AnalyticsUsageViewModel( + analyticsEnabled = analyticsEnabled, + dataStore = dataStore, + selfServerConfig = selfServerConfig, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/analytics/IsAnalyticsAvailableUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/analytics/IsAnalyticsAvailableUseCase.kt index 9844bcbae48..4fe66533ce6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/analytics/IsAnalyticsAvailableUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/analytics/IsAnalyticsAvailableUseCase.kt @@ -23,7 +23,6 @@ import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase -import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.flow.first import javax.inject.Inject @@ -31,7 +30,6 @@ import javax.inject.Inject * UseCase that determines if Analytics is available for current Build and specific [UserId]. * Use it for checking if Analytics UI (e.x. asking user for some feedback that will be sent to Analytics) should be shown to user or not. */ -@ViewModelScoped class IsAnalyticsAvailableUseCase @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, private val analyticsEnabled: AnalyticsConfiguration, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeScreen.kt index 00b5fcc26a9..2fd5cbb8b5d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeScreen.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -75,7 +75,10 @@ import kotlinx.coroutines.job @Composable fun CreateAccountCodeScreen( navigator: Navigator, - createAccountCodeViewModel: CreateAccountCodeViewModel = hiltViewModel() + args: CreateAccountNavArgs, + createAccountCodeViewModel: CreateAccountCodeViewModel = metroViewModel { + createAccountCodeViewModelFactory.create(args) + } ) { with(createAccountCodeViewModel) { fun navigateToSummaryScreen() = navigator.navigate( diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt index 211b90aa4e0..901653b4563 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt @@ -22,10 +22,8 @@ import androidx.compose.foundation.text.input.clearText import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.BuildConfig import com.wire.android.di.ClientScopeProvider import com.wire.android.di.DefaultWebSocketEnabledByDefault @@ -48,15 +46,12 @@ import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.register.RegisterParam import com.wire.kalium.logic.feature.register.RegisterResult import com.wire.kalium.logic.feature.register.RequestActivationCodeResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import javax.inject.Inject // TODO: Cover this viewModel with unit test -@HiltViewModel -class CreateAccountCodeViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class CreateAccountCodeViewModel constructor( + val createAccountNavArgs: CreateAccountNavArgs, @KaliumCoreLogic private val coreLogic: CoreLogic, private val addAuthenticatedUser: AddAuthenticatedUserUseCase, private val clientScopeProviderFactory: ClientScopeProvider.Factory, @@ -64,8 +59,6 @@ class CreateAccountCodeViewModel @Inject constructor( @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean ) : ViewModel() { - val createAccountNavArgs: CreateAccountNavArgs = savedStateHandle.navArgs() - val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig ?: defaultServerConfig val codeTextState: TextFieldState = TextFieldState() diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModelFactory.kt new file mode 100644 index 00000000000..3b04e6c73ee --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModelFactory.kt @@ -0,0 +1,45 @@ +/* + * 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.authentication.create.code + +import com.wire.android.di.ClientScopeProvider +import com.wire.android.di.DefaultWebSocketEnabledByDefault +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +class CreateAccountCodeViewModelFactory( + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val addAuthenticatedUser: AddAuthenticatedUserUseCase, + private val clientScopeProviderFactory: ClientScopeProvider.Factory, + private val defaultServerConfig: ServerConfig.Links, + @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean, +) { + fun create(args: CreateAccountNavArgs): CreateAccountCodeViewModel = CreateAccountCodeViewModel( + createAccountNavArgs = args, + coreLogic = coreLogic, + addAuthenticatedUser = addAuthenticatedUser, + clientScopeProviderFactory = clientScopeProviderFactory, + defaultServerConfig = defaultServerConfig, + defaultWebSocketEnabledByDefault = defaultWebSocketEnabledByDefault, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsScreen.kt index ec620e0e40f..b7accdc8129 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsScreen.kt @@ -44,8 +44,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.authentication.create.common.ServerTitle @@ -74,7 +74,10 @@ import com.wire.kalium.logic.configuration.server.ServerConfig @Composable fun CreateAccountDetailsScreen( navigator: Navigator, - createAccountDetailsViewModel: CreateAccountDetailsViewModel = hiltViewModel() + args: CreateAccountNavArgs, + createAccountDetailsViewModel: CreateAccountDetailsViewModel = metroViewModel { + createAccountDetailsViewModelFactory.create(args) + } ) { with(createAccountDetailsViewModel) { fun navigateToCodeScreen() = navigator.navigate( diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModel.kt index 55e9f6dd7cc..b94f813afb9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModel.kt @@ -21,30 +21,23 @@ import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.ui.authentication.create.common.CreateAccountFlowType import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs import com.wire.android.ui.common.textfield.textAsFlow -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch -import javax.inject.Inject // TODO: Cover this viewModel with unit test -@HiltViewModel -class CreateAccountDetailsViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class CreateAccountDetailsViewModel constructor( + val createAccountNavArgs: CreateAccountNavArgs, private val validatePasswordUseCase: ValidatePasswordUseCase, defaultServerConfig: ServerConfig.Links ) : ViewModel() { - val createAccountNavArgs: CreateAccountNavArgs = savedStateHandle.navArgs() - val firstNameTextState: TextFieldState = TextFieldState() val lastNameTextState: TextFieldState = TextFieldState() val passwordTextState: TextFieldState = TextFieldState() diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelFactory.kt new file mode 100644 index 00000000000..b14c52eed85 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelFactory.kt @@ -0,0 +1,35 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.authentication.create.details + +import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase +import dev.zacsweers.metro.Inject + +@Inject +class CreateAccountDetailsViewModelFactory( + private val validatePasswordUseCase: ValidatePasswordUseCase, + private val defaultServerConfig: ServerConfig.Links, +) { + fun create(args: CreateAccountNavArgs): CreateAccountDetailsViewModel = CreateAccountDetailsViewModel( + createAccountNavArgs = args, + validatePasswordUseCase = validatePasswordUseCase, + defaultServerConfig = defaultServerConfig, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailScreen.kt index f3430c1b9f9..fb7b3d0442d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailScreen.kt @@ -50,8 +50,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -85,7 +85,10 @@ import com.wire.kalium.logic.configuration.server.ServerConfig @Composable fun CreateAccountEmailScreen( navigator: Navigator, - createAccountEmailViewModel: CreateAccountEmailViewModel = hiltViewModel() + args: CreateAccountNavArgs, + createAccountEmailViewModel: CreateAccountEmailViewModel = metroViewModel { + createAccountEmailViewModelFactory.create(args) + } ) { with(createAccountEmailViewModel) { fun navigateToDetailsScreen() = navigator.navigate( diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt index 0dac14da71a..9af4fcc9495 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt @@ -21,34 +21,27 @@ import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.di.KaliumCoreLogic import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs import com.wire.android.ui.common.textfield.textAsFlow -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase import com.wire.kalium.logic.feature.register.RequestActivationCodeResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import javax.inject.Inject // TODO: Cover this viewModel with unit test -@HiltViewModel -class CreateAccountEmailViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class CreateAccountEmailViewModel constructor( + val createAccountNavArgs: CreateAccountNavArgs, private val validateEmail: ValidateEmailUseCase, @KaliumCoreLogic private val coreLogic: CoreLogic, defaultServerConfig: ServerConfig.Links ) : ViewModel() { - val createAccountNavArgs: CreateAccountNavArgs = savedStateHandle.navArgs() - val emailTextState: TextFieldState = TextFieldState() var emailState: CreateAccountEmailViewState by mutableStateOf(CreateAccountEmailViewState(createAccountNavArgs.flowType)) private set diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelFactory.kt new file mode 100644 index 00000000000..764f5ac9a72 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelFactory.kt @@ -0,0 +1,39 @@ +/* + * 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.authentication.create.email + +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase +import dev.zacsweers.metro.Inject + +@Inject +class CreateAccountEmailViewModelFactory( + private val validateEmail: ValidateEmailUseCase, + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val defaultServerConfig: ServerConfig.Links, +) { + fun create(args: CreateAccountNavArgs): CreateAccountEmailViewModel = CreateAccountEmailViewModel( + createAccountNavArgs = args, + validateEmail = validateEmail, + coreLogic = coreLogic, + defaultServerConfig = defaultServerConfig, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModel.kt index 72ffa232d7c..7b8fa26efa0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModel.kt @@ -17,19 +17,13 @@ */ package com.wire.android.ui.authentication.create.overview -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.configuration.server.ServerConfig -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -@HiltViewModel -class CreateAccountOverviewViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class CreateAccountOverviewViewModel constructor( + val navArgs: CreateAccountOverviewNavArgs, defaultServerConfig: ServerConfig.Links ) : ViewModel() { - val navArgs: CreateAccountOverviewNavArgs = savedStateHandle.navArgs() val serverConfig: ServerConfig.Links = navArgs.customServerConfig ?: defaultServerConfig fun learnMoreUrl(): String = serverConfig.pricing } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModelFactory.kt new file mode 100644 index 00000000000..6c7c63900d2 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModelFactory.kt @@ -0,0 +1,31 @@ +/* + * 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.authentication.create.overview + +import com.wire.kalium.logic.configuration.server.ServerConfig +import dev.zacsweers.metro.Inject + +@Inject +class CreateAccountOverviewViewModelFactory( + private val defaultServerConfig: ServerConfig.Links, +) { + fun create(args: CreateAccountOverviewNavArgs): CreateAccountOverviewViewModel = CreateAccountOverviewViewModel( + navArgs = args, + defaultServerConfig = defaultServerConfig, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreatePersonalAccountOverviewScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreatePersonalAccountOverviewScreen.kt index ad13065b0cf..586e4f7e15f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreatePersonalAccountOverviewScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreatePersonalAccountOverviewScreen.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.authentication.create.common.CreateAccountFlowType @@ -64,7 +64,10 @@ import com.wire.kalium.logic.configuration.server.ServerConfig @Composable fun CreatePersonalAccountOverviewScreen( navigator: Navigator, - viewModel: CreateAccountOverviewViewModel = hiltViewModel() + args: CreateAccountOverviewNavArgs, + viewModel: CreateAccountOverviewViewModel = metroViewModel { + createAccountOverviewViewModelFactory.create(args) + } ) { CreateAccountOverviewScreen(navigator, CreateAccountFlowType.CreatePersonalAccount, viewModel) } @@ -73,7 +76,10 @@ fun CreatePersonalAccountOverviewScreen( @Composable fun CreateTeamAccountOverviewScreen( navigator: Navigator, - viewModel: CreateAccountOverviewViewModel = hiltViewModel() + args: CreateAccountOverviewNavArgs, + viewModel: CreateAccountOverviewViewModel = metroViewModel { + createAccountOverviewViewModelFactory.create(args) + } ) { CreateAccountOverviewScreen(navigator, CreateAccountFlowType.CreateTeam, viewModel) } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryScreen.kt index 73184b13b65..dcd8d97c878 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryScreen.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.di.metro.metroViewModel import com.wire.android.R import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -52,7 +52,10 @@ import com.wire.android.ui.theme.wireTypography @Composable fun CreateAccountSummaryScreen( navigator: Navigator, - viewModel: CreateAccountSummaryViewModel = hiltViewModel() + args: CreateAccountSummaryNavArgs, + viewModel: CreateAccountSummaryViewModel = metroViewModel { + createAccountSummaryViewModelFactory.create(args) + } ) { SummaryContent( state = viewModel.summaryState, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryViewModel.kt index e9056f690d6..29b42996069 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryViewModel.kt @@ -21,19 +21,13 @@ package com.wire.android.ui.authentication.create.summary import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import com.wire.android.ui.authentication.create.common.CreateAccountFlowType -import com.ramcosta.composedestinations.generated.app.navArgs -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -@HiltViewModel -class CreateAccountSummaryViewModel @Inject constructor( - savedStateHandle: SavedStateHandle +class CreateAccountSummaryViewModel constructor( + createAccountSummaryNavArgs: CreateAccountSummaryNavArgs ) : ViewModel() { - private val createAccountSummaryNavArgs: CreateAccountSummaryNavArgs = savedStateHandle.navArgs() private val type: CreateAccountFlowType = createAccountSummaryNavArgs.type var summaryState: CreateAccountSummaryViewState by mutableStateOf(CreateAccountSummaryViewState(type)) diff --git a/app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryViewModelFactory.kt similarity index 60% rename from app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt rename to app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryViewModelFactory.kt index f239779156e..bfdd28b3700 100644 --- a/app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryViewModelFactory.kt @@ -15,15 +15,13 @@ * 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.di +package com.wire.android.ui.authentication.create.summary -import androidx.activity.ComponentActivity -import androidx.activity.viewModels -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.withCreationCallback +import dev.zacsweers.metro.Inject -inline fun ComponentActivity.assistedViewModels( - crossinline create: (VMF) -> VM -) = viewModels(extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback { factory -> create(factory) } -}) +@Inject +class CreateAccountSummaryViewModelFactory { + fun create(args: CreateAccountSummaryNavArgs): CreateAccountSummaryViewModel = CreateAccountSummaryViewModel( + createAccountSummaryNavArgs = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameScreen.kt index 3402f67783a..03ab559ca06 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameScreen.kt @@ -32,8 +32,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -58,7 +58,9 @@ import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun CreateAccountUsernameScreen( navigator: Navigator, - viewModel: CreateAccountUsernameViewModel = hiltViewModel() + viewModel: CreateAccountUsernameViewModel = metroViewModel { + createAccountUsernameViewModelFactory.create() + } ) { UsernameContent( textState = viewModel.textState, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameViewModel.kt index d5769d57a30..e1576ad1324 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameViewModel.kt @@ -33,14 +33,11 @@ import com.wire.kalium.logic.feature.auth.ValidateUserHandleResult import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase import com.wire.kalium.logic.feature.user.SetUserHandleResult import com.wire.kalium.logic.feature.user.SetUserHandleUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CreateAccountUsernameViewModel @Inject constructor( +class CreateAccountUsernameViewModel constructor( private val validateUserHandleUseCase: ValidateUserHandleUseCase, private val setUserHandleUseCase: SetUserHandleUseCase, private val finalizeRegistrationAnalyticsMetadata: FinalizeRegistrationAnalyticsMetadataUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameViewModelFactory.kt new file mode 100644 index 00000000000..60124e39b7f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameViewModelFactory.kt @@ -0,0 +1,39 @@ +/* + * 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.authentication.create.username + +import com.wire.android.analytics.FinalizeRegistrationAnalyticsMetadataUseCase +import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase +import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase +import com.wire.kalium.logic.feature.user.SetUserHandleUseCase +import dev.zacsweers.metro.Inject + +@Inject +class CreateAccountUsernameViewModelFactory( + private val validateUserHandleUseCase: ValidateUserHandleUseCase, + private val setUserHandleUseCase: SetUserHandleUseCase, + private val finalizeRegistrationAnalyticsMetadata: FinalizeRegistrationAnalyticsMetadataUseCase, + private val registrationAnalyticsManager: RegistrationAnalyticsManagerUseCase, +) { + fun create(): CreateAccountUsernameViewModel = CreateAccountUsernameViewModel( + validateUserHandleUseCase = validateUserHandleUseCase, + setUserHandleUseCase = setUserHandleUseCase, + finalizeRegistrationAnalyticsMetadata = finalizeRegistrationAnalyticsMetadata, + registrationAnalyticsManager = registrationAnalyticsManager, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/common/ClearSessionViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/common/ClearSessionViewModel.kt index 0268a628e8a..a1da16ce8f7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/common/ClearSessionViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/common/ClearSessionViewModel.kt @@ -32,12 +32,9 @@ import com.wire.kalium.logic.feature.auth.LogoutUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.session.DeleteSessionUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class ClearSessionViewModel @Inject constructor( +class ClearSessionViewModel constructor( private val currentSession: CurrentSessionUseCase, private val deleteSession: DeleteSessionUseCase, private val switchAccount: AccountSwitchUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/common/ClearSessionViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/common/ClearSessionViewModelFactory.kt new file mode 100644 index 00000000000..c36dbddcf37 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/common/ClearSessionViewModelFactory.kt @@ -0,0 +1,39 @@ +/* + * 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.authentication.devices.common + +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import com.wire.kalium.logic.feature.session.DeleteSessionUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ClearSessionViewModelFactory( + private val currentSession: CurrentSessionUseCase, + private val deleteSession: DeleteSessionUseCase, + private val switchAccount: AccountSwitchUseCase, + private val logout: LogoutUseCase, +) { + fun create(): ClearSessionViewModel = ClearSessionViewModel( + currentSession = currentSession, + deleteSession = deleteSession, + switchAccount = switchAccount, + logout = logout, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt index 92cfe6b4b4d..1183dceece3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.NavigationSwitchAccountActions import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.LoginTypeSelector @@ -80,8 +80,8 @@ import com.wire.android.util.ui.PreviewMultipleThemes fun RegisterDeviceScreen( navigator: Navigator, loginTypeSelector: LoginTypeSelector, - viewModel: RegisterDeviceViewModel = hiltViewModel(), - clearSessionViewModel: ClearSessionViewModel = hiltViewModel(), + viewModel: RegisterDeviceViewModel = metroViewModel { registerDeviceViewModelFactory.create() }, + clearSessionViewModel: ClearSessionViewModel = metroViewModel { clearSessionViewModelFactory.create() }, ) { clearAutofillTree() when (val flowState = viewModel.state.flowState) { diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceVerificationCodeScreen.kt index 2b2865a9df1..bed2824fa70 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceVerificationCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceVerificationCodeScreen.kt @@ -20,7 +20,7 @@ package com.wire.android.ui.authentication.devices.register import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Composable -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.authentication.verificationcode.VerificationCodeScreenContent import com.wire.android.ui.authentication.verificationcode.VerificationCodeState import com.wire.android.ui.theme.WireTheme @@ -28,7 +28,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun RegisterDeviceVerificationCodeScreen( - viewModel: RegisterDeviceViewModel = hiltViewModel() + viewModel: RegisterDeviceViewModel = metroViewModel { registerDeviceViewModelFactory.create() } ) = VerificationCodeScreenContent( viewModel.secondFactorVerificationCodeTextState, viewModel.secondFactorVerificationCodeState, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt index 85abe884df1..8f44f674325 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt @@ -38,16 +38,13 @@ import com.wire.kalium.logic.feature.client.RegisterClientParam import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import javax.inject.Inject -@HiltViewModel -class RegisterDeviceViewModel @Inject constructor( +class RegisterDeviceViewModel constructor( private val registerClientUseCase: GetOrRegisterClientUseCase, private val isPasswordRequired: IsPasswordRequiredUseCase, private val userDataStore: UserDataStore, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelFactory.kt new file mode 100644 index 00000000000..ea8800e7816 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelFactory.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.authentication.devices.register + +import com.wire.android.datastore.UserDataStore +import com.wire.android.util.ui.CountdownTimer +import com.wire.kalium.logic.feature.auth.verification.RequestSecondFactorVerificationCodeUseCase +import com.wire.kalium.logic.feature.client.GetOrRegisterClientUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase +import dev.zacsweers.metro.Inject + +@Inject +class RegisterDeviceViewModelFactory( + private val registerClientUseCase: GetOrRegisterClientUseCase, + private val isPasswordRequired: IsPasswordRequiredUseCase, + private val userDataStore: UserDataStore, + private val getSelfUser: GetSelfUserUseCase, + private val requestSecondFactorVerificationCodeUseCase: RequestSecondFactorVerificationCodeUseCase, +) { + fun create(): RegisterDeviceViewModel = RegisterDeviceViewModel( + registerClientUseCase = registerClientUseCase, + isPasswordRequired = isPasswordRequired, + userDataStore = userDataStore, + getSelfUser = getSelfUser, + requestSecondFactorVerificationCodeUseCase = requestSecondFactorVerificationCodeUseCase, + resendCodeTimer = CountdownTimer(), + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt index 5bec132a9dc..65fcc7b9611 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.di.metro.metroViewModel import com.wire.android.R import com.wire.android.feature.NavigationSwitchAccountActions import com.wire.android.navigation.BackStackMode @@ -78,8 +78,8 @@ import com.wire.kalium.logic.data.conversation.ClientId fun RemoveDeviceScreen( navigator: Navigator, loginTypeSelector: LoginTypeSelector, - viewModel: RemoveDeviceViewModel = hiltViewModel(), - clearSessionViewModel: ClearSessionViewModel = hiltViewModel(), + viewModel: RemoveDeviceViewModel = metroViewModel { removeDeviceViewModelFactory.create() }, + clearSessionViewModel: ClearSessionViewModel = metroViewModel { clearSessionViewModelFactory.create() }, ) { fun navigateAfterSuccess(initialSyncCompleted: Boolean, isE2EIRequired: Boolean) = navigator.navigate( NavigationCommand( diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceVerificationCodeScreen.kt index 9d0b564fb61..a3746c48cd6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceVerificationCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceVerificationCodeScreen.kt @@ -18,12 +18,12 @@ package com.wire.android.ui.authentication.devices.remove import androidx.compose.runtime.Composable -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.authentication.verificationcode.VerificationCodeScreenContent @Composable fun RemoveDeviceVerificationCodeScreen( - viewModel: RemoveDeviceViewModel = hiltViewModel() + viewModel: RemoveDeviceViewModel = metroViewModel { removeDeviceViewModelFactory.create() } ) = VerificationCodeScreenContent( viewModel.secondFactorVerificationCodeTextState, viewModel.secondFactorVerificationCodeState, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt index 8895236907d..4d1f4987766 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt @@ -43,17 +43,14 @@ import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.client.SelfClientsResult import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("TooManyFunctions") -@HiltViewModel -class RemoveDeviceViewModel @Inject constructor( +class RemoveDeviceViewModel constructor( private val fetchSelfClientsFromRemote: FetchSelfClientsFromRemoteUseCase, private val deleteClientUseCase: DeleteClientUseCase, private val registerClientUseCase: GetOrRegisterClientUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModelFactory.kt new file mode 100644 index 00000000000..43870777d74 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModelFactory.kt @@ -0,0 +1,48 @@ +/* + * 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.authentication.devices.remove + +import com.wire.android.datastore.UserDataStore +import com.wire.kalium.logic.feature.auth.verification.RequestSecondFactorVerificationCodeUseCase +import com.wire.kalium.logic.feature.client.DeleteClientUseCase +import com.wire.kalium.logic.feature.client.FetchSelfClientsFromRemoteUseCase +import com.wire.kalium.logic.feature.client.GetOrRegisterClientUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase +import dev.zacsweers.metro.Inject + +@Inject +class RemoveDeviceViewModelFactory( + private val fetchSelfClientsFromRemote: FetchSelfClientsFromRemoteUseCase, + private val deleteClientUseCase: DeleteClientUseCase, + private val registerClientUseCase: GetOrRegisterClientUseCase, + private val isPasswordRequired: IsPasswordRequiredUseCase, + private val userDataStore: UserDataStore, + private val getSelfUser: GetSelfUserUseCase, + private val requestSecondFactorVerificationCodeUseCase: RequestSecondFactorVerificationCodeUseCase, +) { + fun create(): RemoveDeviceViewModel = RemoveDeviceViewModel( + fetchSelfClientsFromRemote = fetchSelfClientsFromRemote, + deleteClientUseCase = deleteClientUseCase, + registerClientUseCase = registerClientUseCase, + isPasswordRequired = isPasswordRequired, + userDataStore = userDataStore, + getSelfUser = getSelfUser, + requestSecondFactorVerificationCodeUseCase = requestSecondFactorVerificationCodeUseCase, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginSavedInputStore.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginSavedInputStore.kt new file mode 100644 index 00000000000..c64aab83185 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginSavedInputStore.kt @@ -0,0 +1,24 @@ +/* + * 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.authentication.login + +interface LoginSavedInputStore { + var userIdentifier: String? + var ssoCode: String? +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginSavedInputStoreModule.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginSavedInputStoreModule.kt new file mode 100644 index 00000000000..2cef8b8a028 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginSavedInputStoreModule.kt @@ -0,0 +1,21 @@ +/* + * 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.authentication.login + +// Login saved input store bindings are provided by the Metro graph. diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt index 6df72b5a708..5b673cb6a22 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt @@ -44,12 +44,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.generated.app.destinations.E2EIEnrollmentScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.InitialSyncScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.RemoveDeviceScreenDestination import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -86,7 +86,9 @@ import kotlinx.coroutines.launch fun LoginScreen( navigator: Navigator, loginNavArgs: LoginNavArgs, - loginEmailViewModel: LoginEmailViewModel = hiltViewModel() + loginEmailViewModel: LoginEmailViewModel = metroViewModel { + loginEmailViewModelFactory.create(loginNavArgs) + } ) { LoginContent( @@ -105,6 +107,7 @@ fun LoginScreen( onRemoveDeviceNeeded = { navigator.navigate(NavigationCommand(RemoveDeviceScreenDestination, BackStackMode.CLEAR_WHOLE)) }, + loginNavArgs = loginNavArgs, loginEmailViewModel = loginEmailViewModel, ssoLoginResult = loginNavArgs.ssoLoginResult, ssoCodeAutoLogin = loginNavArgs.ssoCodeAutoLogin @@ -116,6 +119,7 @@ private fun LoginContent( onBackPressed: () -> Unit, onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, + loginNavArgs: LoginNavArgs, loginEmailViewModel: LoginEmailViewModel, ssoLoginResult: DeepLinkResult.SSOLogin?, ssoCodeAutoLogin: SSOCodeAutoLogin?, @@ -139,6 +143,7 @@ private fun LoginContent( onBackPressed = onBackPressed, onSuccess = onSuccess, onRemoveDeviceNeeded = onRemoveDeviceNeeded, + loginNavArgs = loginNavArgs, loginEmailViewModel = loginEmailViewModel, ssoLoginResult = ssoLoginResult, ssoCodeAutoLogin = ssoCodeAutoLogin @@ -154,6 +159,7 @@ private fun MainLoginContent( onBackPressed: () -> Unit, onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, + loginNavArgs: LoginNavArgs, loginEmailViewModel: LoginEmailViewModel, ssoLoginResult: DeepLinkResult.SSOLogin?, ssoCodeAutoLogin: SSOCodeAutoLogin?, @@ -233,6 +239,7 @@ private fun MainLoginContent( LoginTabItem.SSO -> LoginSSOScreen( onSuccess, onRemoveDeviceNeeded, + loginNavArgs, ssoLoginResult, ssoCodeAutoLogin, ) @@ -264,7 +271,10 @@ private fun PreviewLoginScreen() = WireTheme { onBackPressed = {}, onSuccess = { _, _ -> }, onRemoveDeviceNeeded = {}, - loginEmailViewModel = hiltViewModel(), + loginNavArgs = LoginNavArgs(), + loginEmailViewModel = metroViewModel { + loginEmailViewModelFactory.create(LoginNavArgs()) + }, ssoLoginResult = null, ssoCodeAutoLogin = null ) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt index 2feb1ea62ff..a687f0cd20a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt @@ -18,12 +18,9 @@ package com.wire.android.ui.authentication.login -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.ClientScopeProvider -import com.wire.android.di.KaliumCoreLogic -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.data.client.ClientCapability @@ -32,13 +29,10 @@ import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase import com.wire.kalium.logic.feature.auth.AuthenticationResult import com.wire.kalium.logic.feature.auth.DomainLookupUseCase import com.wire.kalium.logic.feature.client.RegisterClientResult -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -@HiltViewModel @Suppress("TooManyFunctions") open class LoginViewModel( - savedStateHandle: SavedStateHandle, + loginNavArgs: LoginNavArgs, val clientScopeProviderFactory: ClientScopeProvider.Factory, val userDataStoreProvider: UserDataStoreProvider, val coreLogic: CoreLogic, @@ -46,23 +40,6 @@ open class LoginViewModel( defaultServerConfig: ServerConfig.Links ) : ViewModel() { - @Inject - constructor( - savedStateHandle: SavedStateHandle, - clientScopeProviderFactory: ClientScopeProvider.Factory, - userDataStoreProvider: UserDataStoreProvider, - @KaliumCoreLogic coreLogic: CoreLogic, - defaultServerConfig: ServerConfig.Links - ) : this( - savedStateHandle, - clientScopeProviderFactory, - userDataStoreProvider, - coreLogic, - LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), - defaultServerConfig - ) - - private val loginNavArgs: LoginNavArgs = savedStateHandle.navArgs() val serverConfig: ServerConfig.Links = loginNavArgs.loginPasswordPath?.customServerConfig ?: defaultServerConfig suspend fun registerClient( diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/SavedStateLoginSavedInputStore.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/SavedStateLoginSavedInputStore.kt new file mode 100644 index 00000000000..93dfb25af61 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/SavedStateLoginSavedInputStore.kt @@ -0,0 +1,40 @@ +/* + * 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.authentication.login + +import androidx.lifecycle.SavedStateHandle + +private const val USER_IDENTIFIER_SAVED_STATE_KEY = "user_identifier" +private const val SSO_CODE_SAVED_STATE_KEY = "sso_code" + +class SavedStateLoginSavedInputStore( + private val savedStateHandle: SavedStateHandle, +) : LoginSavedInputStore { + override var userIdentifier: String? + get() = savedStateHandle[USER_IDENTIFIER_SAVED_STATE_KEY] + set(value) { + savedStateHandle[USER_IDENTIFIER_SAVED_STATE_KEY] = value + } + + override var ssoCode: String? + get() = savedStateHandle[SSO_CODE_SAVED_STATE_KEY] + set(value) { + savedStateHandle[SSO_CODE_SAVED_STATE_KEY] = value + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt index 73a453494bb..d2343846cd9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt @@ -20,7 +20,6 @@ package com.wire.android.ui.authentication.login.email import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Composable -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.ui.authentication.login.LoginState import com.wire.android.ui.authentication.verificationcode.VerificationCodeScreenContent import com.wire.android.ui.authentication.verificationcode.VerificationCodeState @@ -29,7 +28,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun LoginEmailVerificationCodeScreen( - viewModel: LoginEmailViewModel = hiltViewModel() + viewModel: LoginEmailViewModel ) = VerificationCodeScreenContent( viewModel.secondFactorVerificationCodeTextState, viewModel.secondFactorVerificationCodeState, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt index 11ab4c2bfd7..30767b62981 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt @@ -25,21 +25,21 @@ import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.ClientScopeProvider import com.wire.android.di.DefaultWebSocketEnabledByDefault import com.wire.android.di.KaliumCoreLogic import com.wire.android.ui.authentication.login.LoginNavArgs +import com.wire.android.ui.authentication.login.LoginSavedInputStore import com.wire.android.ui.authentication.login.LoginState import com.wire.android.ui.authentication.login.LoginViewModel +import com.wire.android.ui.authentication.login.LoginViewModelExtension import com.wire.android.ui.authentication.login.PreFilledUserIdentifierType import com.wire.android.ui.authentication.login.isProxyAuthRequired import com.wire.android.ui.authentication.login.toLoginError import com.wire.android.ui.authentication.verificationcode.VerificationCodeState import com.wire.android.ui.common.textfield.textAsFlow -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.EMPTY import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.CountdownTimer @@ -58,7 +58,6 @@ import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScop import com.wire.kalium.logic.feature.auth.verification.RequestSecondFactorVerificationCodeUseCase import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.session.CurrentSessionResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest @@ -68,28 +67,28 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject @Suppress("LongParameterList", "ComplexMethod", "TooManyFunctions") -@HiltViewModel -class LoginEmailViewModel @Inject constructor( +class LoginEmailViewModel constructor( + val loginNavArgs: LoginNavArgs, private val addAuthenticatedUser: AddAuthenticatedUserUseCase, clientScopeProviderFactory: ClientScopeProvider.Factory, - private val savedStateHandle: SavedStateHandle, + private val savedInputStore: LoginSavedInputStore, userDataStoreProvider: UserDataStoreProvider, @KaliumCoreLogic coreLogic: CoreLogic, private val resendCodeTimer: CountdownTimer, private val dispatchers: DispatcherProvider, defaultServerConfig: ServerConfig.Links, @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean, + private val sharedAuthLoginEmailAdapter: SharedAuthLoginEmailAdapter = LegacySharedAuthLoginEmailAdapter, ) : LoginViewModel( - savedStateHandle, + loginNavArgs, clientScopeProviderFactory, userDataStoreProvider, coreLogic, + LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), defaultServerConfig ) { - val loginNavArgs: LoginNavArgs = savedStateHandle.navArgs() private val preFilledUserIdentifier: PreFilledUserIdentifierType = loginNavArgs.userHandle ?: PreFilledUserIdentifierType.None val userIdentifierTextState: TextFieldState = TextFieldState() @@ -110,13 +109,13 @@ class LoginEmailViewModel @Inject constructor( if (preFilledUserIdentifier is PreFilledUserIdentifierType.PreFilled) { preFilledUserIdentifier.userIdentifier } else { - savedStateHandle[USER_IDENTIFIER_SAVED_STATE_KEY] ?: String.EMPTY + savedInputStore.userIdentifier ?: String.EMPTY } ) viewModelScope.launch { combine( userIdentifierTextState.textAsFlow().distinctUntilChanged().onEach { - savedStateHandle[USER_IDENTIFIER_SAVED_STATE_KEY] = it.toString() + savedInputStore.userIdentifier = it.toString() }, passwordTextState.textAsFlow(), proxyIdentifierTextState.textAsFlow(), @@ -162,6 +161,7 @@ class LoginEmailViewModel @Inject constructor( null } } + if (tryLoginWithSharedAuth(usernameAllowed)) return@launch // first, cancel and revert any previous login if it's still running, just to be sure revertLogin() // then, start a new login job @@ -174,6 +174,30 @@ class LoginEmailViewModel @Inject constructor( } } + private suspend fun tryLoginWithSharedAuth(usernameAllowed: Boolean): Boolean = + sharedAuthLoginEmailAdapter.tryLogin( + request = SharedAuthLoginEmailRequest( + userIdentifier = userIdentifierTextState.text.toString(), + password = passwordTextState.text.toString(), + secondFactorVerificationCode = secondFactorVerificationCodeTextState.text.toString(), + usernameAllowed = usernameAllowed, + serverConfig = serverConfig, + ), + callbacks = object : SharedAuthLoginEmailCallbacks { + override suspend fun updateFlowState(flowState: LoginState) { + updateEmailFlowState(flowState) + } + + override suspend fun updateSecondFactorState(update: (VerificationCodeState) -> VerificationCodeState) { + secondFactorVerificationCodeState = update(secondFactorVerificationCodeState) + } + + override suspend fun startResendCodeTimer() { + this@LoginEmailViewModel.startResendCodeTimer() + } + } + ) + @Suppress("LongMethod") private fun startLoginJob(usernameAllowed: Boolean): Job { return viewModelScope.launch { @@ -404,7 +428,6 @@ class LoginEmailViewModel @Inject constructor( } companion object { - const val USER_IDENTIFIER_SAVED_STATE_KEY = "user_identifier" const val RESEND_TIMER_DELAY = 300L } } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelFactory.kt new file mode 100644 index 00000000000..60c383d9d4a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelFactory.kt @@ -0,0 +1,58 @@ +/* + * 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.authentication.login.email + +import com.wire.android.datastore.UserDataStoreProvider +import com.wire.android.di.ClientScopeProvider +import com.wire.android.di.DefaultWebSocketEnabledByDefault +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.ui.authentication.login.LoginNavArgs +import com.wire.android.ui.authentication.login.LoginSavedInputStore +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.android.util.ui.CountdownTimer +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class LoginEmailViewModelFactory( + private val addAuthenticatedUser: AddAuthenticatedUserUseCase, + private val clientScopeProviderFactory: ClientScopeProvider.Factory, + private val savedInputStore: LoginSavedInputStore, + private val userDataStoreProvider: UserDataStoreProvider, + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val resendCodeTimer: CountdownTimer, + private val dispatchers: DispatcherProvider, + private val defaultServerConfig: ServerConfig.Links, + @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean, +) { + fun create(args: LoginNavArgs): LoginEmailViewModel = LoginEmailViewModel( + loginNavArgs = args, + addAuthenticatedUser = addAuthenticatedUser, + clientScopeProviderFactory = clientScopeProviderFactory, + savedInputStore = savedInputStore, + userDataStoreProvider = userDataStoreProvider, + coreLogic = coreLogic, + resendCodeTimer = resendCodeTimer, + dispatchers = dispatchers, + defaultServerConfig = defaultServerConfig, + defaultWebSocketEnabledByDefault = defaultWebSocketEnabledByDefault, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/SharedAuthLoginEmailAdapter.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/SharedAuthLoginEmailAdapter.kt new file mode 100644 index 00000000000..317c53c874c --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/SharedAuthLoginEmailAdapter.kt @@ -0,0 +1,57 @@ +/* + * 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.authentication.login.email + +import com.wire.android.ui.authentication.login.LoginState +import com.wire.android.ui.authentication.verificationcode.VerificationCodeState +import com.wire.kalium.logic.configuration.server.ServerConfig + +/** + * Android-side boundary for replacing the email/password login step with shared/auth. + * + * A future implementation should adapt shared/auth state and effects to the existing Android UI contract. + * Android keeps text fields, resources, navigation, proxy form rendering and screen lifecycle ownership. + */ +fun interface SharedAuthLoginEmailAdapter { + suspend fun tryLogin( + request: SharedAuthLoginEmailRequest, + callbacks: SharedAuthLoginEmailCallbacks, + ): Boolean +} + +data class SharedAuthLoginEmailRequest( + val userIdentifier: String, + val password: String, + val secondFactorVerificationCode: String, + val usernameAllowed: Boolean, + val serverConfig: ServerConfig.Links, +) + +interface SharedAuthLoginEmailCallbacks { + suspend fun updateFlowState(flowState: LoginState) + suspend fun updateSecondFactorState(update: (VerificationCodeState) -> VerificationCodeState) + suspend fun startResendCodeTimer() +} + +object LegacySharedAuthLoginEmailAdapter : SharedAuthLoginEmailAdapter { + override suspend fun tryLogin( + request: SharedAuthLoginEmailRequest, + callbacks: SharedAuthLoginEmailCallbacks, + ): Boolean = false +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt index 98b46e9579b..b4a94c94ba5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -42,8 +41,8 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.authentication.login.LoginErrorDialog import com.wire.android.ui.authentication.login.LoginState import com.wire.android.ui.authentication.login.toLoginDialogErrorData @@ -64,9 +63,12 @@ import kotlinx.coroutines.flow.onEach fun LoginSSOScreen( onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, + loginNavArgs: com.wire.android.ui.authentication.login.LoginNavArgs, ssoLoginResult: DeepLinkResult.SSOLogin?, ssoCodeAutoLogin: com.wire.android.ui.authentication.login.SSOCodeAutoLogin?, - loginSSOViewModel: LoginSSOViewModel = hiltViewModel(), + loginSSOViewModel: LoginSSOViewModel = metroViewModel { + loginSSOViewModelFactory.create(loginNavArgs) + }, scrollState: ScrollState = rememberScrollState() ) { val scope = rememberCoroutineScope() @@ -81,13 +83,12 @@ fun LoginSSOScreen( // Handle SSO code auto-login from intent parameter LaunchedEffect(ssoCodeAutoLogin) { ssoCodeAutoLogin?.let { - // Pre-fill the SSO code - loginSSOViewModel.ssoTextState.setTextAndPlaceCursorAtEnd(it.ssoCode) - - // Auto-initiate login if flag is set - if (it.autoInitiateLogin) { - loginSSOViewModel.login() - } + loginSSOViewModel.handleSSOCodeAutoLogin( + ssoCode = it.ssoCode, + autoInitiateLogin = it.autoInitiateLogin, + nomadServiceUrl = it.nomadServiceUrl, + cookieLabel = it.cookieLabel, + ) } } LoginSSOContent( diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOSessionExceptionClassifier.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOSessionExceptionClassifier.kt new file mode 100644 index 00000000000..7436c28401a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOSessionExceptionClassifier.kt @@ -0,0 +1,34 @@ +/* + * 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.authentication.login.sso + +import android.database.sqlite.SQLiteException +import java.io.IOException +import javax.inject.Inject + +class LoginSSOSessionExceptionClassifier @Inject constructor() { + + fun isRecoverableSessionException(exception: Exception): Boolean = when (exception) { + is IllegalStateException, + is IOException, + is SQLiteException -> true + + else -> false + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt index 2e755ae0335..eef862c1c78 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt @@ -24,9 +24,7 @@ import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.appLogger import com.wire.android.config.DefaultServerConfig import com.wire.android.datastore.UserDataStoreProvider @@ -34,8 +32,10 @@ import com.wire.android.di.ClientScopeProvider import com.wire.android.di.DefaultWebSocketEnabledByDefault import com.wire.android.di.KaliumCoreLogic import com.wire.android.ui.authentication.login.LoginNavArgs +import com.wire.android.ui.authentication.login.LoginSavedInputStore import com.wire.android.ui.authentication.login.LoginState import com.wire.android.ui.authentication.login.LoginViewModel +import com.wire.android.ui.authentication.login.LoginViewModelExtension import com.wire.android.ui.authentication.login.toLoginError import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState import com.wire.android.ui.common.textfield.textAsFlow @@ -57,43 +57,54 @@ import com.wire.kalium.logic.feature.auth.sso.SSOLoginSessionResult import com.wire.kalium.logic.feature.backup.RestoreCryptoStateResult import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.session.DoesValidSessionExistResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import android.database.sqlite.SQLiteException -import java.io.IOException -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class LoginSSOViewModel( - private val savedStateHandle: SavedStateHandle, - val addAuthenticatedUser: AddAuthenticatedUserUseCase, - private val validateEmailUseCase: ValidateEmailUseCase, - coreLogic: CoreLogic, - clientScopeProviderFactory: ClientScopeProvider.Factory, - userDataStoreProvider: UserDataStoreProvider, - private val ssoExtension: LoginSSOViewModelExtension, - serverConfig: ServerConfig.Links, - private val dispatchers: DispatcherProvider, -) : LoginViewModel( - savedStateHandle, - clientScopeProviderFactory, - userDataStoreProvider, - coreLogic, - serverConfig -) { - private val loginNavArgs: LoginNavArgs = savedStateHandle.navArgs() - private var pendingNomadServiceUrl: String? = loginNavArgs.ssoCodeAutoLogin?.nomadServiceUrl - private var pendingCookieLabel: String? = loginNavArgs.ssoCodeAutoLogin?.cookieLabel - - @Inject +class LoginSSOViewModel : LoginViewModel { + private val savedInputStore: LoginSavedInputStore + val addAuthenticatedUser: AddAuthenticatedUserUseCase + private val validateEmailUseCase: ValidateEmailUseCase + private val ssoExtension: LoginSSOViewModelExtension + private val sessionExceptionClassifier: LoginSSOSessionExceptionClassifier + private val dispatchers: DispatcherProvider + + private var pendingNomadServiceUrl: String? = null + private var pendingCookieLabel: String? = null + constructor( - savedStateHandle: SavedStateHandle, + loginNavArgs: LoginNavArgs, + savedInputStore: LoginSavedInputStore, + addAuthenticatedUser: AddAuthenticatedUserUseCase, + validateEmailUseCase: ValidateEmailUseCase, + coreLogic: CoreLogic, + clientScopeProviderFactory: ClientScopeProvider.Factory, + userDataStoreProvider: UserDataStoreProvider, + serverConfig: ServerConfig.Links, + ssoExtension: LoginSSOViewModelExtension, + sessionExceptionClassifier: LoginSSOSessionExceptionClassifier, + dispatchers: DispatcherProvider, + ) : this( + loginNavArgs, + savedInputStore, + addAuthenticatedUser, + validateEmailUseCase, + coreLogic, + clientScopeProviderFactory, + userDataStoreProvider, + ssoExtension, + serverConfig, + sessionExceptionClassifier, + dispatchers, + ) + + constructor( + loginNavArgs: LoginNavArgs, + savedInputStore: LoginSavedInputStore, addAuthenticatedUser: AddAuthenticatedUserUseCase, validateEmailUseCase: ValidateEmailUseCase, @KaliumCoreLogic coreLogic: CoreLogic, @@ -101,32 +112,64 @@ class LoginSSOViewModel( userDataStoreProvider: UserDataStoreProvider, serverConfig: ServerConfig.Links, @DefaultWebSocketEnabledByDefault defaultWebSocketEnabledByDefault: Boolean, + sessionExceptionClassifier: LoginSSOSessionExceptionClassifier, dispatchers: DispatcherProvider, ) : this( - savedStateHandle, + loginNavArgs, + savedInputStore, addAuthenticatedUser, validateEmailUseCase, coreLogic, clientScopeProviderFactory, userDataStoreProvider, - LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic, defaultWebSocketEnabledByDefault), serverConfig, + LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic, defaultWebSocketEnabledByDefault), + sessionExceptionClassifier, dispatchers, ) + private constructor( + loginNavArgs: LoginNavArgs, + savedInputStore: LoginSavedInputStore, + addAuthenticatedUser: AddAuthenticatedUserUseCase, + validateEmailUseCase: ValidateEmailUseCase, + coreLogic: CoreLogic, + clientScopeProviderFactory: ClientScopeProvider.Factory, + userDataStoreProvider: UserDataStoreProvider, + ssoExtension: LoginSSOViewModelExtension, + serverConfig: ServerConfig.Links, + sessionExceptionClassifier: LoginSSOSessionExceptionClassifier, + dispatchers: DispatcherProvider, + ) : super( + loginNavArgs, + clientScopeProviderFactory, + userDataStoreProvider, + coreLogic, + LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), + serverConfig + ) { + this.savedInputStore = savedInputStore + this.addAuthenticatedUser = addAuthenticatedUser + this.validateEmailUseCase = validateEmailUseCase + this.ssoExtension = ssoExtension + this.sessionExceptionClassifier = sessionExceptionClassifier + this.dispatchers = dispatchers + observeSSOCodeInput() + } + var openWebUrl = MutableSharedFlow>() val ssoTextState: TextFieldState = TextFieldState() var loginState: LoginSSOState by mutableStateOf(LoginSSOState()) - init { - ssoTextState.setTextAndPlaceCursorAtEnd(savedStateHandle[SSO_CODE_SAVED_STATE_KEY] ?: String.EMPTY) + private fun observeSSOCodeInput() { + ssoTextState.setTextAndPlaceCursorAtEnd(savedInputStore.ssoCode ?: String.EMPTY) viewModelScope.launch { ssoTextState.textAsFlow().distinctUntilChanged().collectLatest { if (loginState.flowState != LoginState.Loading) { updateSSOFlowState(LoginState.Default) } - savedStateHandle[SSO_CODE_SAVED_STATE_KEY] = it.toString() + savedInputStore.ssoCode = it.toString() } } } @@ -184,6 +227,21 @@ class LoginSSOViewModel( } } + fun handleSSOCodeAutoLogin( + ssoCode: String, + autoInitiateLogin: Boolean, + nomadServiceUrl: String?, + cookieLabel: String?, + ) { + pendingNomadServiceUrl = nomadServiceUrl + pendingCookieLabel = cookieLabel + ssoTextState.setTextAndPlaceCursorAtEnd(ssoCode) + + if (autoInitiateLogin) { + login() + } + } + @VisibleForTesting fun domainLookupFlow() { viewModelScope.launch { @@ -319,14 +377,12 @@ class LoginSSOViewModel( } catch (e: CancellationException) { throw e } catch (e: Exception) { - when (e) { - is IllegalStateException, is IOException, is SQLiteException -> { - if (isSessionStillValid(storedUserId)) throw e - appLogger.w("$TAG Crypto restore interrupted by concurrent logout: ${e.message}") - return - } - else -> throw e + if (sessionExceptionClassifier.isRecoverableSessionException(e)) { + if (isSessionStillValid(storedUserId)) throw e + appLogger.w("$TAG Crypto restore interrupted by concurrent logout: ${e.message}") + return } + throw e } when (restoreResult) { @@ -356,12 +412,11 @@ class LoginSSOViewModel( } catch (e: CancellationException) { throw e } catch (e: Exception) { - when (e) { - is IllegalStateException, is IOException, is SQLiteException -> { - if (isSessionStillValid(userId)) throw e - appLogger.w("$TAG Failed to revert SSO session, may have been already logged out: ${e.message}") - } - else -> throw e + if (sessionExceptionClassifier.isRecoverableSessionException(e)) { + if (isSessionStillValid(userId)) throw e + appLogger.w("$TAG Failed to revert SSO session, may have been already logged out: ${e.message}") + } else { + throw e } } } @@ -374,7 +429,6 @@ class LoginSSOViewModel( } companion object { - const val SSO_CODE_SAVED_STATE_KEY = "sso_code" private const val TAG = "[LoginSSOViewModel]" } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelFactory.kt new file mode 100644 index 00000000000..79ef1e7d9de --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelFactory.kt @@ -0,0 +1,60 @@ +/* + * 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.authentication.login.sso + +import com.wire.android.datastore.UserDataStoreProvider +import com.wire.android.di.ClientScopeProvider +import com.wire.android.di.DefaultWebSocketEnabledByDefault +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.ui.authentication.login.LoginNavArgs +import com.wire.android.ui.authentication.login.LoginSavedInputStore +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase +import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class LoginSSOViewModelFactory( + private val savedInputStore: LoginSavedInputStore, + private val addAuthenticatedUser: AddAuthenticatedUserUseCase, + private val validateEmailUseCase: ValidateEmailUseCase, + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val clientScopeProviderFactory: ClientScopeProvider.Factory, + private val userDataStoreProvider: UserDataStoreProvider, + private val serverConfig: ServerConfig.Links, + @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean, + private val sessionExceptionClassifier: LoginSSOSessionExceptionClassifier, + private val dispatchers: DispatcherProvider, +) { + fun create(args: LoginNavArgs): LoginSSOViewModel = LoginSSOViewModel( + loginNavArgs = args, + savedInputStore = savedInputStore, + addAuthenticatedUser = addAuthenticatedUser, + validateEmailUseCase = validateEmailUseCase, + coreLogic = coreLogic, + clientScopeProviderFactory = clientScopeProviderFactory, + userDataStoreProvider = userDataStoreProvider, + serverConfig = serverConfig, + defaultWebSocketEnabledByDefault = defaultWebSocketEnabledByDefault, + sessionExceptionClassifier = sessionExceptionClassifier, + dispatchers = dispatchers, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt index a13bb803bf5..1de8ce113df 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt @@ -66,10 +66,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.BuildConfig.ENABLE_NEW_REGISTRATION import com.wire.android.R import com.wire.android.config.LocalCustomUiConfigurationProvider +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.PopUpNavigationAnimation @@ -113,7 +113,10 @@ import kotlinx.coroutines.flow.scan @Composable fun WelcomeScreen( navigator: Navigator, - viewModel: WelcomeViewModel = hiltViewModel() + args: WelcomeNavArgs, + viewModel: WelcomeViewModel = metroViewModel { + welcomeViewModelFactory.create(args) + } ) { WelcomeContent( viewModel.state.isThereActiveSession, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModel.kt index fc27d5821fd..ab16a1f24f0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModel.kt @@ -21,28 +21,22 @@ package com.wire.android.ui.authentication.welcome import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.BuildConfig -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.feature.session.DoesValidNomadAccountExistUseCase import com.wire.kalium.logic.feature.session.GetAllSessionsResult import com.wire.kalium.logic.feature.session.GetSessionsUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class WelcomeViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class WelcomeViewModel constructor( + navArgs: WelcomeNavArgs, private val getSessions: GetSessionsUseCase, private val doesValidNomadAccountExist: DoesValidNomadAccountExistUseCase, defaultServerConfig: ServerConfig.Links ) : ViewModel() { - private val navArgs: WelcomeNavArgs = savedStateHandle.navArgs() var state by mutableStateOf(WelcomeScreenState(navArgs.customServerConfig ?: defaultServerConfig)) private set diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelFactory.kt new file mode 100644 index 00000000000..aa75eeba085 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelFactory.kt @@ -0,0 +1,37 @@ +/* + * 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.authentication.welcome + +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.feature.session.DoesValidNomadAccountExistUseCase +import com.wire.kalium.logic.feature.session.GetSessionsUseCase +import dev.zacsweers.metro.Inject + +@Inject +class WelcomeViewModelFactory( + private val getSessions: GetSessionsUseCase, + private val doesValidNomadAccountExist: DoesValidNomadAccountExistUseCase, + private val defaultServerConfig: ServerConfig.Links, +) { + fun create(args: WelcomeNavArgs): WelcomeViewModel = WelcomeViewModel( + navArgs = args, + getSessions = getSessions, + doesValidNomadAccountExist = doesValidNomadAccountExist, + defaultServerConfig = defaultServerConfig, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt index 12321dea6e1..2ac73b7aead 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt @@ -23,7 +23,6 @@ import android.os.Bundle import android.view.WindowManager import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets @@ -36,36 +35,43 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory import com.wire.android.appLogger -import com.wire.android.di.assistedViewModels +import com.wire.android.di.metro.LocalMetroViewModelGraph +import com.wire.android.di.metro.WireMetroGraph +import com.wire.android.di.metro.createWireMetroGraph import com.wire.android.ui.AppLockActivity import com.wire.android.ui.BaseActivity import com.wire.android.ui.LocalActivity -import com.wire.android.ui.calling.common.ProximitySensorManager import com.wire.android.ui.common.setupOrientationForDevice import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.CommonTopAppBarParams import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel import com.wire.android.ui.common.topappbar.WireTopAppBar import com.wire.android.ui.theme.WireTheme -import com.wire.android.util.SwitchAccountObserver import com.wire.kalium.logic.data.id.QualifiedIdMapper -import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import javax.inject.Inject -@AndroidEntryPoint abstract class CallActivity : BaseActivity() { - @Inject - lateinit var switchAccountObserver: SwitchAccountObserver - - @Inject - lateinit var proximitySensorManager: ProximitySensorManager + protected val metroGraph by lazy(LazyThreadSafetyMode.NONE) { + createWireMetroGraph(this) + } + private val switchAccountObserver by lazy(LazyThreadSafetyMode.NONE) { + metroGraph.switchAccountObserver + } + protected val proximitySensorManager by lazy(LazyThreadSafetyMode.NONE) { + metroGraph.proximitySensorManager + } - private val commonTopAppBarViewModel by assistedViewModels { factory -> - factory.create(CommonTopAppBarParams(showNoNetwork = true, showSync = false, showActiveCalls = false)) + private val commonTopAppBarViewModel: CommonTopAppBarViewModel by metroActivityViewModel { + commonTopAppBarViewModelFactory.create( + CommonTopAppBarParams(showNoNetwork = true, showSync = false, showActiveCalls = false) + ) } companion object { @@ -76,7 +82,9 @@ abstract class CallActivity : BaseActivity() { const val TAG = "CallActivity" } - private val callActivityViewModel: CallActivityViewModel by viewModels() + private val callActivityViewModel: CallActivityViewModel by metroActivityViewModel { + callActivityViewModelFactory.create() + } protected val qualifiedIdMapper = QualifiedIdMapper(null) override fun onNewIntent(intent: Intent) { @@ -102,7 +110,8 @@ abstract class CallActivity : BaseActivity() { val snackbarHostState = remember { SnackbarHostState() } CompositionLocalProvider( LocalSnackbarHostState provides snackbarHostState, - LocalActivity provides this + LocalActivity provides this, + LocalMetroViewModelGraph provides metroGraph, ) { WireTheme { Column( @@ -188,4 +197,13 @@ abstract class CallActivity : BaseActivity() { } } } + + private inline fun metroActivityViewModel( + crossinline create: WireMetroGraph.() -> VM, + ): Lazy = lazy(LazyThreadSafetyMode.NONE) { + val factory = viewModelFactory { + initializer { metroGraph.create() } + } + ViewModelProvider(this, factory)[VM::class.java] + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModel.kt index 004667bb057..b2d59a913a8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModel.kt @@ -28,16 +28,13 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CallActivityViewModel @Inject constructor( +class CallActivityViewModel( private val dispatchers: DispatcherProvider, private val currentSession: CurrentSessionUseCase, private val observeScreenshotCensoringConfigUseCaseProviderFactory: diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModelFactory.kt new file mode 100644 index 00000000000..3efcd51536a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModelFactory.kt @@ -0,0 +1,40 @@ +/* + * 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.calling + +import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import dev.zacsweers.metro.Inject + +@Inject +class CallActivityViewModelFactory( + private val dispatchers: DispatcherProvider, + private val currentSession: CurrentSessionUseCase, + private val observeScreenshotCensoringConfigUseCaseProviderFactory: + ObserveScreenshotCensoringConfigUseCaseProvider.Factory, + private val accountSwitch: AccountSwitchUseCase, +) { + fun create(): CallActivityViewModel = CallActivityViewModel( + dispatchers = dispatchers, + currentSession = currentSession, + observeScreenshotCensoringConfigUseCaseProviderFactory = observeScreenshotCensoringConfigUseCaseProviderFactory, + accountSwitch = accountSwitch, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/StartingCallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/StartingCallActivity.kt index 327c1d70b17..1c947c4c602 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/StartingCallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/StartingCallActivity.kt @@ -37,7 +37,6 @@ import com.wire.android.ui.calling.CallActivity.Companion.EXTRA_USER_ID import com.wire.android.ui.calling.incoming.IncomingCallScreen import com.wire.android.ui.calling.ongoing.getOngoingCallIntent import com.wire.android.ui.calling.outgoing.OutgoingCallScreen -import dagger.hilt.android.AndroidEntryPoint /** * Activity that handles starting call screens, Incoming and Outgoing @@ -49,7 +48,6 @@ import dagger.hilt.android.AndroidEntryPoint * @see OutgoingCallScreen */ @OptIn(ExperimentalComposeUiApi::class) -@AndroidEntryPoint class StartingCallActivity : CallActivity() { private var conversationId: String? by mutableStateOf(null) private var userId: String? by mutableStateOf(null) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/common/ProximitySensorManager.kt b/app/src/main/kotlin/com/wire/android/ui/calling/common/ProximitySensorManager.kt index 5d28838d929..d03b36da13d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/common/ProximitySensorManager.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/common/ProximitySensorManager.kt @@ -32,6 +32,7 @@ import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import dagger.Lazy +import com.wire.android.di.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -39,8 +40,8 @@ import javax.inject.Singleton @Singleton class ProximitySensorManager @Inject constructor( - private val context: Context, - private val currentSession: Lazy, + @ApplicationContext private val context: Context, + private val currentSession: CurrentSessionUseCase, @KaliumCoreLogic private val coreLogic: Lazy, @ApplicationScope private val appCoroutineScope: CoroutineScope ) { @@ -74,7 +75,7 @@ class ProximitySensorManager @Inject constructor( override fun onSensorChanged(event: SensorEvent) { appCoroutineScope.launch { coreLogic.get().globalScope { - val currentSession = currentSession.get().invoke() + val currentSession = currentSession.invoke() when { currentSession is CurrentSessionResult.Success && currentSession.accountInfo.isValid() -> { val userId = currentSession.accountInfo.userId diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/common/SharedCallingViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/common/SharedCallingViewModel.kt index 641b9706f06..2a07e924e0e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/common/SharedCallingViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/common/SharedCallingViewModel.kt @@ -18,8 +18,6 @@ package com.wire.android.ui.calling.common -import android.view.View -import androidx.camera.core.impl.ImageOutputConfig.RotationValue import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -52,10 +50,6 @@ import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.util.PlatformRotation import com.wire.kalium.logic.util.PlatformView -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted @@ -68,9 +62,8 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel(assistedFactory = SharedCallingViewModel.Factory::class) -class SharedCallingViewModel @AssistedInject constructor( - @Assisted val conversationId: ConversationId, +class SharedCallingViewModel( + val conversationId: ConversationId, private val conversationDetails: ObserveConversationDetailsUseCase, private val observeLastActiveCallWithSortedParticipants: ObserveLastActiveCallWithSortedParticipantsUseCase, private val hangUpCall: HangUpCallUseCase, @@ -238,25 +231,20 @@ class SharedCallingViewModel @AssistedInject constructor( } } - fun setVideoPreview(view: View?) { + fun setVideoPreview(view: PlatformView) { viewModelScope.launch(dispatchers.default()) { appLogger.i("SharedCallingViewModel: setting video preview..") setVideoPreview(conversationId, PlatformView(null)) - setVideoPreview(conversationId, PlatformView(view)) + setVideoPreview(conversationId, view) } } - fun setUIRotation(@RotationValue rotation: Int) { + fun setUIRotation(rotation: PlatformRotation) { appLogger.i("SharedCallingViewModel: setting UI rotation to $rotation..") viewModelScope.launch { - setUIRotationUseCase(PlatformRotation(rotation)) + setUIRotationUseCase(rotation) } } - - @AssistedFactory - interface Factory { - fun create(conversationId: ConversationId): SharedCallingViewModel - } } sealed interface SharedCallingViewActions { diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/common/SharedCallingViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/calling/common/SharedCallingViewModelFactory.kt new file mode 100644 index 00000000000..16b582d1973 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/common/SharedCallingViewModelFactory.kt @@ -0,0 +1,75 @@ +/* + * 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.calling.common + +import com.wire.android.mapper.UserTypeMapper +import com.wire.android.ui.calling.usecase.HangUpCallUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.call.usecase.FlipToBackCameraUseCase +import com.wire.kalium.logic.feature.call.usecase.FlipToFrontCameraUseCase +import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveLastActiveCallWithSortedParticipantsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveSpeakerUseCase +import com.wire.kalium.logic.feature.call.usecase.SetUIRotationUseCase +import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase +import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase +import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase +import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase +import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class SharedCallingViewModelFactory( + private val conversationDetails: ObserveConversationDetailsUseCase, + private val observeLastActiveCallWithSortedParticipants: ObserveLastActiveCallWithSortedParticipantsUseCase, + private val hangUpCall: HangUpCallUseCase, + private val muteCall: MuteCallUseCase, + private val unMuteCall: UnMuteCallUseCase, + private val updateVideoState: UpdateVideoStateUseCase, + private val setVideoPreview: SetVideoPreviewUseCase, + private val setUIRotationUseCase: SetUIRotationUseCase, + private val turnLoudSpeakerOff: TurnLoudSpeakerOffUseCase, + private val turnLoudSpeakerOn: TurnLoudSpeakerOnUseCase, + private val flipToFrontCamera: FlipToFrontCameraUseCase, + private val flipToBackCamera: FlipToBackCameraUseCase, + private val observeSpeaker: ObserveSpeakerUseCase, + private val userTypeMapper: UserTypeMapper, + private val dispatchers: DispatcherProvider, +) { + fun create(conversationId: ConversationId): SharedCallingViewModel = SharedCallingViewModel( + conversationId = conversationId, + conversationDetails = conversationDetails, + observeLastActiveCallWithSortedParticipants = observeLastActiveCallWithSortedParticipants, + hangUpCall = hangUpCall, + muteCall = muteCall, + unMuteCall = unMuteCall, + updateVideoState = updateVideoState, + setVideoPreview = setVideoPreview, + setUIRotationUseCase = setUIRotationUseCase, + turnLoudSpeakerOff = turnLoudSpeakerOff, + turnLoudSpeakerOn = turnLoudSpeakerOn, + flipToFrontCamera = flipToFrontCamera, + flipToBackCamera = flipToBackCamera, + observeSpeaker = observeSpeaker, + userTypeMapper = userTypeMapper, + dispatchers = dispatchers, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt index 8793c6cd4c6..64bc87ef62d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt @@ -35,12 +35,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import com.wire.android.R import com.wire.android.appLogger +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.LocalActivity import com.wire.android.ui.calling.CallActivity import com.wire.android.ui.calling.common.CallVideoPreview @@ -64,21 +64,21 @@ import com.wire.android.ui.theme.wireTypography import com.wire.android.util.permission.rememberRecordAudioPermissionFlow import com.wire.kalium.logic.data.call.ConversationTypeForCall import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.util.PlatformRotation +import com.wire.kalium.logic.util.PlatformView @Suppress("ParameterWrapping") @Composable fun IncomingCallScreen( conversationId: ConversationId, shouldTryToAnswerCallAutomatically: Boolean, - incomingCallViewModel: IncomingCallViewModel = hiltViewModel( - key = "incoming_$conversationId", - creationCallback = { factory -> factory.create(conversationId = conversationId) } - ), + incomingCallViewModel: IncomingCallViewModel = metroViewModel(key = "incoming_$conversationId") { + incomingCallViewModelFactory.create(conversationId = conversationId) + }, sharedCallingViewModel: SharedCallingViewModel = - hiltViewModel( - key = "shared_$conversationId", - creationCallback = { factory -> factory.create(conversationId = conversationId) } - ), + metroViewModel(key = "shared_$conversationId") { + sharedCallingViewModelFactory.create(conversationId = conversationId) + }, onCallAccepted: () -> Unit ) { val activity = LocalActivity.current @@ -152,7 +152,7 @@ fun IncomingCallScreen( toggleVideo = ::toggleVideo, declineCall = incomingCallViewModel::declineCall, acceptCall = audioPermissionCheck::launch, - onVideoPreviewCreated = ::setVideoPreview, + onVideoPreviewCreated = { view -> setVideoPreview(PlatformView(view)) }, onSelfClearVideoPreview = ::clearVideoPreview, onCameraPermissionPermanentlyDenied = { permissionPermanentlyDeniedDialogState.show( @@ -166,7 +166,7 @@ fun IncomingCallScreen( activity.moveTaskToBack(true) } ) - ObserveRotation(::setUIRotation) + ObserveRotation { rotation -> setUIRotation(PlatformRotation(rotation)) } } PermissionPermanentlyDeniedDialog( diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt index e77795a40f5..e7af55674c5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.viewModelScope -import com.wire.android.di.CurrentAccount import com.wire.android.notification.CallNotificationManager import com.wire.android.ui.calling.incoming.IncomingCallState.WaitingUnlockState import com.wire.android.ui.common.ActionsViewModel @@ -35,10 +34,6 @@ import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.RejectCallUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -49,10 +44,9 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @Suppress("LongParameterList") -@HiltViewModel(assistedFactory = IncomingCallViewModel.Factory::class) -class IncomingCallViewModel @AssistedInject constructor( - @Assisted val conversationId: ConversationId, - @CurrentAccount val currentAccount: UserId, +class IncomingCallViewModel( + val conversationId: ConversationId, + val currentAccount: UserId, private var callNotificationManager: CallNotificationManager, private val incomingCalls: GetIncomingCallsUseCase, private val rejectCall: RejectCallUseCase, @@ -203,11 +197,6 @@ class IncomingCallViewModel @AssistedInject constructor( companion object { const val DELAY_END_CALL = 200L } - - @AssistedFactory - interface Factory { - fun create(conversationId: ConversationId): IncomingCallViewModel - } } sealed interface IncomingCallViewActions { diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelFactory.kt new file mode 100644 index 00000000000..c4f65fe3390 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelFactory.kt @@ -0,0 +1,58 @@ +/* + * 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.calling.incoming + +import com.wire.android.di.CurrentAccount +import com.wire.android.notification.CallNotificationManager +import com.wire.android.ui.home.appLock.LockCodeTimeManager +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase +import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase +import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.RejectCallUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class IncomingCallViewModelFactory( + @CurrentAccount private val currentAccount: UserId, + private val callNotificationManager: CallNotificationManager, + private val incomingCalls: GetIncomingCallsUseCase, + private val rejectCall: RejectCallUseCase, + private val acceptCall: AnswerCallUseCase, + private val muteCall: MuteCallUseCase, + private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, + private val endCall: EndCallUseCase, + private val lockCodeTimeManager: LockCodeTimeManager, +) { + fun create(conversationId: ConversationId): IncomingCallViewModel = IncomingCallViewModel( + conversationId = conversationId, + currentAccount = currentAccount, + callNotificationManager = callNotificationManager, + incomingCalls = incomingCalls, + rejectCall = rejectCall, + acceptCall = acceptCall, + muteCall = muteCall, + observeEstablishedCalls = observeEstablishedCalls, + endCall = endCall, + lockCodeTimeManager = lockCodeTimeManager, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallActivity.kt index 936a7265a58..8a350eb1d7e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallActivity.kt @@ -38,9 +38,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId import com.wire.android.R import com.wire.android.appLogger import com.wire.android.navigation.style.TransitionAnimationType -import com.wire.android.notification.CallNotificationManager import com.wire.android.notification.endOngoingCallPendingIntent -import com.wire.android.services.ServicesManager import com.wire.android.ui.calling.CallActivity import com.wire.android.ui.calling.CallActivity.Companion.EXTRA_CONVERSATION_ID import com.wire.android.ui.calling.CallActivity.Companion.EXTRA_SHOULD_ANSWER_CALL @@ -48,8 +46,6 @@ import com.wire.android.ui.calling.CallActivity.Companion.EXTRA_USER_ID import com.wire.android.ui.calling.ongoing.OngoingCallActivity.Companion.TAG import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject /** * Activity that handles ongoing call screen, Ongoing. @@ -60,13 +56,13 @@ import javax.inject.Inject * @see OngoingCallScreen */ @OptIn(ExperimentalComposeUiApi::class) -@AndroidEntryPoint class OngoingCallActivity : CallActivity() { - @Inject - lateinit var servicesManager: ServicesManager - - @Inject - lateinit var callNotificationManager: CallNotificationManager + private val servicesManager by lazy(LazyThreadSafetyMode.NONE) { + metroGraph.servicesManager + } + private val callNotificationManager by lazy(LazyThreadSafetyMode.NONE) { + metroGraph.callNotificationManager + } var conversationId: String? by mutableStateOf(null) var userId: String? by mutableStateOf(null) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt index 776d3019106..b9df2ff1934 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt @@ -73,13 +73,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.zIndex -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner import com.wire.android.BuildConfig import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.LocalActivity import com.wire.android.ui.calling.common.ObservePictureInPictureMode import com.wire.android.ui.calling.common.ObserveRotation @@ -143,6 +143,8 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.SecurityClassificationType +import com.wire.kalium.logic.util.PlatformRotation +import com.wire.kalium.logic.util.PlatformView import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -154,15 +156,13 @@ import java.util.Locale @Composable fun OngoingCallScreen( conversationId: ConversationId, - ongoingCallViewModel: OngoingCallViewModel = hiltViewModel( - key = "ongoing_$conversationId", - creationCallback = { factory -> factory.create(conversationId = conversationId) } - ), + ongoingCallViewModel: OngoingCallViewModel = metroViewModel(key = "ongoing_$conversationId") { + ongoingCallViewModelFactory.create(conversationId = conversationId) + }, sharedCallingViewModel: SharedCallingViewModel = - hiltViewModel( - key = "shared_$conversationId", - creationCallback = { factory -> factory.create(conversationId = conversationId) } - ) + metroViewModel(key = "shared_$conversationId") { + sharedCallingViewModelFactory.create(conversationId = conversationId) + } ) { val scope = rememberCoroutineScope() val permissionPermanentlyDeniedDialogState = rememberVisibilityState() @@ -256,7 +256,7 @@ fun OngoingCallScreen( hangUpCall = sharedCallingViewModel::hangUpCall, toggleVideo = sharedCallingViewModel::toggleVideo, flipCamera = sharedCallingViewModel::flipCamera, - setVideoPreview = sharedCallingViewModel::setVideoPreview, + setVideoPreview = { view -> sharedCallingViewModel.setVideoPreview(PlatformView(view)) }, clearVideoPreview = sharedCallingViewModel::clearVideoPreview, onCollapse = onCollapse, requestVideoStreams = ongoingCallViewModel::requestVideoStreams, @@ -275,7 +275,7 @@ fun OngoingCallScreen( toasts = ongoingCallViewModel.toasts.values.toSet(), onToastClick = ongoingCallViewModel::dismissToast, ) - ObserveRotation(sharedCallingViewModel::setUIRotation) + ObserveRotation { rotation -> sharedCallingViewModel.setUIRotation(PlatformRotation(rotation)) } /** * Enter PiP mode when the user leaves the app by pressing the home button. diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt index 28bb9cf539a..a9f7d99b60d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt @@ -27,7 +27,6 @@ import androidx.lifecycle.viewModelScope import com.wire.android.BuildConfig import com.wire.android.appLogger import com.wire.android.datastore.GlobalDataStore -import com.wire.android.di.CurrentAccount import com.wire.android.mapper.UICallParticipantMapper import com.wire.android.ui.calling.model.InCallReaction import com.wire.android.ui.calling.model.ReactionSender @@ -62,10 +61,6 @@ import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase import com.wire.kalium.logic.feature.incallreaction.SendInCallReactionUseCase import com.wire.kalium.network.NetworkState import com.wire.kalium.network.NetworkStateObserver -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel @@ -87,10 +82,9 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel(assistedFactory = OngoingCallViewModel.Factory::class) -class OngoingCallViewModel @AssistedInject constructor( - @Assisted val conversationId: ConversationId, - @CurrentAccount val currentUserId: UserId, +class OngoingCallViewModel( + val conversationId: ConversationId, + val currentUserId: UserId, private val globalDataStore: GlobalDataStore, private val networkStateObserver: NetworkStateObserver, private val observeLastActiveCall: ObserveLastActiveCallWithSortedParticipantsUseCase, @@ -375,11 +369,6 @@ class OngoingCallViewModel @AssistedInject constructor( const val DELAY_TO_SHOW_DOUBLE_TAP_TOAST = 500L const val TAG = "OngoingCallViewModel" } - - @AssistedFactory - interface Factory { - fun create(conversationId: ConversationId): OngoingCallViewModel - } } private fun List.senderName(userId: QualifiedID) = firstOrNull { it.id.value == userId.value }?.name diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModelFactory.kt new file mode 100644 index 00000000000..26d52f44704 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModelFactory.kt @@ -0,0 +1,73 @@ +/* + * 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.calling.ongoing + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.di.CurrentAccount +import com.wire.android.mapper.UICallParticipantMapper +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.call.usecase.ObserveCallModerationActionsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveCallQualityDataUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveInCallReactionsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveLastActiveCallWithSortedParticipantsUseCase +import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase +import com.wire.kalium.logic.feature.call.usecase.SetCallQualityIntervalUseCase +import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase +import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase +import com.wire.kalium.logic.feature.incallreaction.SendInCallReactionUseCase +import com.wire.kalium.network.NetworkStateObserver +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class OngoingCallViewModelFactory( + @CurrentAccount private val currentUserId: UserId, + private val globalDataStore: GlobalDataStore, + private val networkStateObserver: NetworkStateObserver, + private val observeLastActiveCall: ObserveLastActiveCallWithSortedParticipantsUseCase, + private val requestVideoStreams: RequestVideoStreamsUseCase, + private val setVideoSendState: SetVideoSendStateUseCase, + private val observeCallQualityData: ObserveCallQualityDataUseCase, + private val setCallQualityInterval: SetCallQualityIntervalUseCase, + private val getCurrentClientId: ObserveCurrentClientIdUseCase, + private val observeInCallReactionsUseCase: ObserveInCallReactionsUseCase, + private val sendInCallReactionUseCase: SendInCallReactionUseCase, + private val observeCallModerationActions: ObserveCallModerationActionsUseCase, + private val uiCallParticipantMapper: UICallParticipantMapper, + private val dispatchers: DispatcherProvider, +) { + fun create(conversationId: ConversationId): OngoingCallViewModel = OngoingCallViewModel( + conversationId = conversationId, + currentUserId = currentUserId, + globalDataStore = globalDataStore, + networkStateObserver = networkStateObserver, + observeLastActiveCall = observeLastActiveCall, + requestVideoStreams = requestVideoStreams, + setVideoSendState = setVideoSendState, + observeCallQualityData = observeCallQualityData, + setCallQualityInterval = setCallQualityInterval, + getCurrentClientId = getCurrentClientId, + observeInCallReactionsUseCase = observeInCallReactionsUseCase, + sendInCallReactionUseCase = sendInCallReactionUseCase, + observeCallModerationActions = observeCallModerationActions, + uiCallParticipantMapper = uiCallParticipantMapper, + dispatchers = dispatchers, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt index 6da855f5c49..de7541eb7cd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.LocalActivity import com.wire.android.ui.calling.common.CallVideoPreview import com.wire.android.ui.calling.common.CallerDetails @@ -50,20 +50,20 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.util.PlatformRotation +import com.wire.kalium.logic.util.PlatformView @Suppress("ParameterWrapping") @Composable fun OutgoingCallScreen( conversationId: ConversationId, sharedCallingViewModel: SharedCallingViewModel = - hiltViewModel( - key = "shared_$conversationId", - creationCallback = { factory -> factory.create(conversationId = conversationId) } - ), - outgoingCallViewModel: OutgoingCallViewModel = hiltViewModel( - key = "outgoing_$conversationId", - creationCallback = { factory -> factory.create(conversationId = conversationId) } - ), + metroViewModel(key = "shared_$conversationId") { + sharedCallingViewModelFactory.create(conversationId = conversationId) + }, + outgoingCallViewModel: OutgoingCallViewModel = metroViewModel(key = "outgoing_$conversationId") { + outgoingCallViewModelFactory.create(conversationId = conversationId) + }, onCallAccepted: () -> Unit ) { val permissionPermanentlyDeniedDialogState = @@ -93,7 +93,7 @@ fun OutgoingCallScreen( toggleSpeaker = ::toggleSpeaker, toggleVideo = ::toggleVideo, onHangUpCall = outgoingCallViewModel::hangUpCall, - onVideoPreviewCreated = ::setVideoPreview, + onVideoPreviewCreated = { view -> setVideoPreview(PlatformView(view)) }, onSelfClearVideoPreview = ::clearVideoPreview, onCameraPermissionPermanentlyDenied = { permissionPermanentlyDeniedDialogState.show( @@ -107,7 +107,7 @@ fun OutgoingCallScreen( activity.moveTaskToBack(true) } ) - ObserveRotation(::setUIRotation) + ObserveRotation { rotation -> setUIRotation(PlatformRotation(rotation)) } } PermissionPermanentlyDeniedDialog( diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt index 0e6d5fa24ae..e618fb1268a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt @@ -32,10 +32,6 @@ import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -44,9 +40,8 @@ import org.jetbrains.annotations.VisibleForTesting import java.util.Calendar @Suppress("LongParameterList") -@HiltViewModel(assistedFactory = OutgoingCallViewModel.Factory::class) -class OutgoingCallViewModel @AssistedInject constructor( - @Assisted val conversationId: ConversationId, +class OutgoingCallViewModel( + val conversationId: ConversationId, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val observeOutgoingCall: ObserveOutgoingCallUseCase, private val startCall: StartCallUseCase, @@ -133,9 +128,4 @@ class OutgoingCallViewModel @AssistedInject constructor( state = state.copy(flowState = OutgoingCallState.FlowState.CallClosed) } } - - @AssistedFactory - interface Factory { - fun create(conversationId: ConversationId): OutgoingCallViewModel - } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModelFactory.kt new file mode 100644 index 00000000000..ec171185527 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModelFactory.kt @@ -0,0 +1,47 @@ +/* + * 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.calling.outgoing + +import com.wire.android.media.CallRinger +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase +import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase +import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase +import dev.zacsweers.metro.Inject + +@Inject +class OutgoingCallViewModelFactory( + private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, + private val observeOutgoingCall: ObserveOutgoingCallUseCase, + private val startCall: StartCallUseCase, + private val endCall: EndCallUseCase, + private val isLastCallClosed: IsLastCallClosedUseCase, + private val callRinger: CallRinger, +) { + fun create(conversationId: ConversationId): OutgoingCallViewModel = OutgoingCallViewModel( + conversationId = conversationId, + observeEstablishedCalls = observeEstablishedCalls, + observeOutgoingCall = observeOutgoingCall, + startCall = startCall, + endCall = endCall, + isLastCallClosed = isLastCallClosed, + callRinger = callRinger, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/usecase/HangUpCallUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/calling/usecase/HangUpCallUseCase.kt index b2b969c3a1b..73de585e560 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/usecase/HangUpCallUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/usecase/HangUpCallUseCase.kt @@ -26,13 +26,11 @@ import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveSpeakerUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase -import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import javax.inject.Inject -@ViewModelScoped class HangUpCallUseCase @Inject constructor( @ApplicationScope private val coroutineScope: CoroutineScope, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/AddContactButton.kt b/app/src/main/kotlin/com/wire/android/ui/common/AddContactButton.kt index dcce69c69a2..721ef700f6c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/AddContactButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/AddContactButton.kt @@ -22,12 +22,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.ui.common.button.WireSecondaryIconButton import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.snackbar.collectAndShowSnackbar import com.wire.android.ui.connection.ConnectionActionButtonArgs import com.wire.android.ui.connection.ConnectionActionButtonViewModel +import com.wire.android.ui.connection.ConnectionActionButtonViewModelFactory import com.wire.android.ui.connection.ConnectionActionButtonViewModelImpl import com.wire.android.ui.connection.ConnectionActionState import com.wire.android.ui.connection.MissingLegalHoldConsentDialogState @@ -42,11 +43,11 @@ fun AddContactButton( userName: String, modifier: Modifier = Modifier, viewModel: ConnectionActionButtonViewModel = - hiltViewModelScoped< + wireViewModelScoped< ConnectionActionButtonViewModelImpl, ConnectionActionButtonViewModel, ConnectionActionButtonArgs, - ConnectionActionButtonViewModelImpl.Factory + ConnectionActionButtonViewModelFactory >( ConnectionActionButtonArgs(userId, userName) ), diff --git a/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationBanner.kt b/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationBanner.kt index 77cb72b4c1f..10aaf0fa9f3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationBanner.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationBanner.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.theme.WireTheme @@ -56,11 +56,11 @@ fun SecurityClassificationBannerForConversation( conversationId: ConversationId, modifier: Modifier = Modifier, viewModel: SecurityClassificationViewModel = - hiltViewModelScoped< + wireViewModelScoped< SecurityClassificationViewModelImpl, SecurityClassificationViewModel, SecurityClassificationArgs, - SecurityClassificationViewModelImpl.Factory + SecurityClassificationViewModelFactory >(SecurityClassificationArgs.Conversation(conversationId)) ) { SecurityClassificationBanner( @@ -74,11 +74,11 @@ fun SecurityClassificationBannerForUser( userId: UserId, modifier: Modifier = Modifier, viewModel: SecurityClassificationViewModel = - hiltViewModelScoped< + wireViewModelScoped< SecurityClassificationViewModelImpl, SecurityClassificationViewModel, SecurityClassificationArgs, - SecurityClassificationViewModelImpl.Factory + SecurityClassificationViewModelFactory >( SecurityClassificationArgs.User(id = userId) ) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationViewModel.kt index 22357a52d68..ce23912531c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationViewModel.kt @@ -23,18 +23,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.di.KaliumCoreLogic -import com.wire.android.di.AssistedViewModelFactory import com.wire.android.di.ViewModelScopedPreview import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.SecurityClassificationType import com.wire.kalium.logic.feature.session.CurrentSessionResult -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch @@ -44,10 +38,9 @@ interface SecurityClassificationViewModel { fun state(): SecurityClassificationType = SecurityClassificationType.NONE } -@HiltViewModel(assistedFactory = SecurityClassificationViewModelImpl.Factory::class) -class SecurityClassificationViewModelImpl @AssistedInject constructor( - @KaliumCoreLogic private val coreLogic: CoreLogic, - @Assisted private val args: SecurityClassificationArgs +class SecurityClassificationViewModelImpl( + private val coreLogic: CoreLogic, + private val args: SecurityClassificationArgs ) : SecurityClassificationViewModel, ViewModel() { private var state by mutableStateOf(SecurityClassificationType.NONE) @@ -85,9 +78,4 @@ class SecurityClassificationViewModelImpl @AssistedInject constructor( private suspend fun observeUserClassificationType(currentUserId: UserId, userId: UserId) = coreLogic.getSessionScope(currentUserId).getOtherUserSecurityClassificationLabel(userId) - - @AssistedFactory - interface Factory : AssistedViewModelFactory { - override fun create(args: SecurityClassificationArgs): SecurityClassificationViewModelImpl - } } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationViewModelFactory.kt new file mode 100644 index 00000000000..39009466148 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationViewModelFactory.kt @@ -0,0 +1,33 @@ +/* + * 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.common.banner + +import com.wire.android.di.KaliumCoreLogic +import com.wire.kalium.logic.CoreLogic +import dev.zacsweers.metro.Inject + +@Inject +class SecurityClassificationViewModelFactory( + @KaliumCoreLogic private val coreLogic: CoreLogic, +) { + fun create(args: SecurityClassificationArgs): SecurityClassificationViewModelImpl = + SecurityClassificationViewModelImpl( + coreLogic = coreLogic, + args = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModel.kt index 74ea32b1794..93a90df68ed 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModel.kt @@ -23,7 +23,6 @@ import androidx.lifecycle.viewModelScope import androidx.work.WorkManager import com.wire.android.BuildConfig import com.wire.android.appLogger -import com.wire.android.di.CurrentAccount import com.wire.android.di.ViewModelScopedPreview import com.wire.android.model.SnackBarMessage import com.wire.android.ui.common.ActionsManager @@ -64,7 +63,6 @@ import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase import com.wire.kalium.logic.feature.team.Result import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import com.wire.kalium.util.DateTimeUtil -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -80,7 +78,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject @ViewModelScopedPreview interface ConversationOptionsMenuViewModel : ActionsManager { @@ -110,9 +107,8 @@ interface ConversationOptionsMenuViewModel : ActionsManager Unit = {}, openConversationDebugMenu: (ConversationId) -> Unit = {}, viewModel: ConversationOptionsMenuViewModel = - hiltViewModelScoped() + wireViewModelScoped< + ConversationOptionsMenuViewModelImpl, + ConversationOptionsMenuViewModel, + ConversationOptionsMenuViewModelFactory, + >() ) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current @@ -189,6 +193,7 @@ private fun PreviewConversationOptionsModalSheetLayout(initialPage: Conversation onLeftConversation = {}, onDeletedConversation = {}, onDeletedConversationLocally = {}, + viewModel = object : ConversationOptionsMenuViewModel {}, ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt index 20755ae437c..88748c43e61 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt @@ -37,10 +37,6 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.network.NetworkState import dagger.Lazy -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -51,11 +47,10 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import org.jetbrains.annotations.VisibleForTesting -@HiltViewModel(assistedFactory = CommonTopAppBarViewModel.Factory::class) -class CommonTopAppBarViewModel @AssistedInject constructor( +class CommonTopAppBarViewModel( private val currentScreenManager: CurrentScreenManager, @KaliumCoreLogic private val coreLogic: Lazy, - @Assisted private val params: CommonTopAppBarParams, + private val params: CommonTopAppBarParams, ) : ViewModel() { var state by mutableStateOf(CommonTopAppBarState()) @@ -194,11 +189,6 @@ class CommonTopAppBarViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(params: CommonTopAppBarParams): CommonTopAppBarViewModel - } - private companion object { const val CONNECTIVITY_STATE_DEBOUNCE_ONGOING_CALL = 600L const val CONNECTIVITY_STATE_DEBOUNCE_DEFAULT = 1000L diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelFactory.kt new file mode 100644 index 00000000000..39cfac7e316 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.common.topappbar + +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.util.CurrentScreenManager +import com.wire.kalium.logic.CoreLogic +import dagger.Lazy +import dev.zacsweers.metro.Inject + +@Inject +class CommonTopAppBarViewModelFactory( + private val currentScreenManager: CurrentScreenManager, + @KaliumCoreLogic private val coreLogic: Lazy, +) { + fun create(params: CommonTopAppBarParams): CommonTopAppBarViewModel = CommonTopAppBarViewModel( + currentScreenManager = currentScreenManager, + coreLogic = coreLogic, + params = params, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt index 46a694a6e0b..77cd9287498 100644 --- a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButton.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.model.ClickBlockParams import com.wire.android.ui.common.HandleActions import com.wire.android.ui.common.VisibilityState @@ -62,6 +62,7 @@ import com.wire.kalium.logic.data.user.UserId const val CONNECTION_ACTION_BUTTONS_TEST_TAG = "connection_buttons" +@Suppress("CyclomaticComplexMethod") @Composable fun ConnectionActionButton( userId: UserId, @@ -73,11 +74,11 @@ fun ConnectionActionButton( onConnectionRequestIgnored: (String) -> Unit = {}, onOpenConversation: (ConversationId) -> Unit = {}, viewModel: ConnectionActionButtonViewModel = - hiltViewModelScoped< + wireViewModelScoped< ConnectionActionButtonViewModelImpl, ConnectionActionButtonViewModel, ConnectionActionButtonArgs, - ConnectionActionButtonViewModelImpl.Factory + ConnectionActionButtonViewModelFactory >( ConnectionActionButtonArgs(userId, userName) ), diff --git a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModel.kt index b4fd11103fe..43cae70d904 100644 --- a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModel.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.appLogger -import com.wire.android.di.AssistedViewModelFactory import com.wire.android.di.ViewModelScopedPreview import com.wire.android.ui.common.ActionsManager import com.wire.android.ui.common.ActionsViewModel @@ -45,10 +44,6 @@ import com.wire.kalium.logic.feature.connection.UnblockUserResult import com.wire.kalium.logic.feature.connection.UnblockUserUseCase import com.wire.kalium.logic.feature.conversation.CreateConversationResult import com.wire.kalium.logic.feature.conversation.GetOrCreateOneToOneConversationUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -69,8 +64,7 @@ interface ConnectionActionButtonViewModel : ActionsManager() { private val userId: QualifiedID = args.userId @@ -91,11 +85,6 @@ internal class ConnectionActionButtonViewModelImpl @AssistedInject constructor( override fun actionableState(): ConnectionActionState = state - @AssistedFactory - interface Factory : AssistedViewModelFactory { - override fun create(args: ConnectionActionButtonArgs): ConnectionActionButtonViewModelImpl - } - override fun onSendConnectionRequest() { if (state.isPerformingAction) return state = state.performAction() diff --git a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelFactory.kt new file mode 100644 index 00000000000..d853e4e2bdd --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelFactory.kt @@ -0,0 +1,53 @@ +/* + * 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.connection + +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.connection.AcceptConnectionRequestUseCase +import com.wire.kalium.logic.feature.connection.CancelConnectionRequestUseCase +import com.wire.kalium.logic.feature.connection.IgnoreConnectionRequestUseCase +import com.wire.kalium.logic.feature.connection.SendConnectionRequestUseCase +import com.wire.kalium.logic.feature.connection.UnblockUserUseCase +import com.wire.kalium.logic.feature.conversation.GetOrCreateOneToOneConversationUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class ConnectionActionButtonViewModelFactory( + private val dispatchers: DispatcherProvider, + private val sendConnectionRequest: SendConnectionRequestUseCase, + private val cancelConnectionRequest: CancelConnectionRequestUseCase, + private val acceptConnectionRequest: AcceptConnectionRequestUseCase, + private val ignoreConnectionRequest: IgnoreConnectionRequestUseCase, + private val unblockUser: UnblockUserUseCase, + private val getOrCreateOneToOneConversation: GetOrCreateOneToOneConversationUseCase, +) { + fun create(args: ConnectionActionButtonArgs): ConnectionActionButtonViewModel = createImpl(args) + + internal fun createImpl(args: ConnectionActionButtonArgs): ConnectionActionButtonViewModelImpl = + ConnectionActionButtonViewModelImpl( + dispatchers = dispatchers, + sendConnectionRequest = sendConnectionRequest, + cancelConnectionRequest = cancelConnectionRequest, + acceptConnectionRequest = acceptConnectionRequest, + ignoreConnectionRequest = ignoreConnectionRequest, + unblockUser = unblockUser, + getOrCreateOneToOneConversation = getOrCreateOneToOneConversation, + args = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt new file mode 100644 index 00000000000..8691d612779 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt @@ -0,0 +1,36 @@ +/* + * 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.debug + +import android.content.Context +import com.wire.android.util.getDeviceIdString +import com.wire.android.util.getGitBuildId +import com.wire.android.di.ApplicationContext +import dev.zacsweers.metro.Inject + +interface DebugDataInfoProvider { + fun deviceId(): String? + fun gitBuildId(): String +} + +class AndroidDebugDataInfoProvider @Inject constructor( + @ApplicationContext private val context: Context +) : DebugDataInfoProvider { + override fun deviceId(): String? = context.getDeviceIdString() + override fun gitBuildId(): String = context.getGitBuildId() +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt index ddbe202fc30..f00de508210 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.wire.android.BuildConfig import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.model.Clickable import com.wire.android.ui.common.WireDialog @@ -70,7 +70,7 @@ fun DebugDataOptions( onShowFeatureFlags: () -> Unit, onShowCryptoStats: () -> Unit, viewModel: DebugDataOptionsViewModel = - hiltViewModelScoped() + wireViewModelScoped() ) { LocalSnackbarHostState.current.collectAndShowSnackbar(snackbarFlow = viewModel.infoMessage) DebugDataOptionsContent( diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt index 6d7842c07c2..76cd253318f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.debug -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.getValue @@ -30,8 +29,6 @@ import com.wire.android.appLogger import com.wire.android.di.CurrentAccount import com.wire.android.di.ViewModelScopedPreview import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.getDeviceIdString -import com.wire.android.util.getGitBuildId import com.wire.android.util.ui.UIText import com.wire.android.util.uiText import com.wire.kalium.logic.configuration.server.CommonApiVersionType @@ -58,8 +55,6 @@ import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -68,7 +63,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject import kotlin.time.Duration.Companion.days @Suppress("TooManyFunctions") @@ -94,10 +88,8 @@ interface DebugDataOptionsViewModel { } @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class DebugDataOptionsViewModelImpl -@Inject constructor( - @ApplicationContext private val context: Context, +class DebugDataOptionsViewModelImpl( + private val debugDataInfoProvider: DebugDataInfoProvider, @CurrentAccount val currentAccount: UserId, private val updateApiVersions: UpdateApiVersionsScheduler, private val mlsKeyPackageCount: MLSKeyPackageCountUseCase, @@ -186,11 +178,9 @@ class DebugDataOptionsViewModelImpl private fun setGitHashAndDeviceId() { viewModelScope.launch { - val deviceId = context.getDeviceIdString() ?: "null" - val gitBuildId = context.getGitBuildId() state = state.copy( - debugId = deviceId, - commitish = gitBuildId + debugId = debugDataInfoProvider.deviceId() ?: "null", + commitish = debugDataInfoProvider.gitBuildId() ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModelFactory.kt new file mode 100644 index 00000000000..df339b8908e --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModelFactory.kt @@ -0,0 +1,76 @@ +/* + * 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.debug + +import com.wire.android.di.CurrentAccount +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.analytics.GetCurrentAnalyticsTrackingIdentifierUseCase +import com.wire.kalium.logic.feature.debug.GetDebugE2EICertificateExpirationUseCase +import com.wire.kalium.logic.feature.debug.ObserveIsConsumableNotificationsEnabledUseCase +import com.wire.kalium.logic.feature.debug.RepairFaultyRemovalKeysUseCase +import com.wire.kalium.logic.feature.debug.SetDebugE2EICertificateExpirationUseCase +import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsUseCase +import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase +import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase +import com.wire.kalium.logic.feature.notificationToken.SendFCMTokenUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase +import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase +import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler +import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class DebugDataOptionsViewModelFactory( + private val debugDataInfoProvider: DebugDataInfoProvider, + @CurrentAccount private val currentAccount: UserId, + private val updateApiVersions: UpdateApiVersionsScheduler, + private val mlsKeyPackageCount: MLSKeyPackageCountUseCase, + private val restartSlowSyncProcessForRecovery: RestartSlowSyncProcessForRecoveryUseCase, + private val checkCrlRevocationList: CheckCrlRevocationListUseCase, + private val getCurrentAnalyticsTrackingIdentifier: GetCurrentAnalyticsTrackingIdentifierUseCase, + private val sendFCMToken: SendFCMTokenUseCase, + private val dispatcherProvider: DispatcherProvider, + private val selfServerConfigUseCase: SelfServerConfigUseCase, + private val getDefaultProtocolUseCase: GetDefaultProtocolUseCase, + private val observeAsyncNotificationsEnabled: ObserveIsConsumableNotificationsEnabledUseCase, + private val startUsingAsyncNotifications: StartUsingAsyncNotificationsUseCase, + private val repairFaultyRemovalKeys: RepairFaultyRemovalKeysUseCase, + private val getDebugE2EICertificateExpiration: GetDebugE2EICertificateExpirationUseCase, + private val setDebugE2EICertificateExpiration: SetDebugE2EICertificateExpirationUseCase, +) { + fun create(): DebugDataOptionsViewModelImpl = DebugDataOptionsViewModelImpl( + debugDataInfoProvider = debugDataInfoProvider, + currentAccount = currentAccount, + updateApiVersions = updateApiVersions, + mlsKeyPackageCount = mlsKeyPackageCount, + restartSlowSyncProcessForRecovery = restartSlowSyncProcessForRecovery, + checkCrlRevocationList = checkCrlRevocationList, + getCurrentAnalyticsTrackingIdentifier = getCurrentAnalyticsTrackingIdentifier, + sendFCMToken = sendFCMToken, + dispatcherProvider = dispatcherProvider, + selfServerConfigUseCase = selfServerConfigUseCase, + getDefaultProtocolUseCase = getDefaultProtocolUseCase, + observeAsyncNotificationsEnabled = observeAsyncNotificationsEnabled, + startUsingAsyncNotifications = startUsingAsyncNotifications, + repairFaultyRemovalKeys = repairFaultyRemovalKeys, + getDebugE2EICertificateExpiration = getDebugE2EICertificateExpiration, + setDebugE2EICertificateExpiration = setDebugE2EICertificateExpiration, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt index d9e86cea49c..7019052bc8f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt @@ -41,10 +41,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.BuildConfig import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.metro.metroViewModel +import com.wire.android.di.wireViewModelScoped import com.wire.android.model.Clickable import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -73,7 +73,9 @@ import java.io.File @Composable fun DebugScreen( navigator: Navigator, - userDebugViewModel: UserDebugViewModel = hiltViewModel(), + userDebugViewModel: UserDebugViewModel = metroViewModel { + userDebugViewModelFactory.create() + }, ) { UserDebugContent( onNavigationPressed = navigator::navigateBack, @@ -149,7 +151,7 @@ internal fun UserDebugContent( fun DangerOptions( modifier: Modifier = Modifier, exportObfuscatedCopyViewModel: ExportObfuscatedCopyViewModel = - hiltViewModelScoped() + wireViewModelScoped() ) { Column(modifier = modifier) { @@ -168,7 +170,7 @@ fun DangerOptions( backUpAndRestoreState = exportObfuscatedCopyViewModel.state, backupPasswordTextState = exportObfuscatedCopyViewModel.createBackupPasswordState, onCreateBackup = exportObfuscatedCopyViewModel::createObfuscatedCopy, - onSaveBackup = exportObfuscatedCopyViewModel::saveCopy, + onSaveBackup = { uri -> exportObfuscatedCopyViewModel.saveCopy(uri.toString()) }, onShareBackup = exportObfuscatedCopyViewModel::shareCopy, onCancelCreateBackup = { backupAndRestoreStateHolder.dismissDialog() diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyFileGateway.kt b/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyFileGateway.kt new file mode 100644 index 00000000000..7a85d94ce99 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyFileGateway.kt @@ -0,0 +1,45 @@ +/* + * 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.debug + +import androidx.core.net.toUri +import com.wire.android.util.FileManager +import com.wire.android.util.dispatchers.DispatcherProvider +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.withContext +import okio.Path + +interface ExportObfuscatedCopyFileGateway { + suspend fun shareCopy(path: Path, assetName: String?) + suspend fun saveCopy(path: Path, destinationUri: String) +} + +class AndroidExportObfuscatedCopyFileGateway @Inject constructor( + private val fileManager: FileManager, + private val dispatcher: DispatcherProvider, +) : ExportObfuscatedCopyFileGateway { + + override suspend fun shareCopy(path: Path, assetName: String?) = withContext(dispatcher.io()) { + fileManager.shareWithExternalApp(path, assetName) {} + } + + override suspend fun saveCopy(path: Path, destinationUri: String) { + fileManager.copyToUri(path, destinationUri.toUri(), dispatcher) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModel.kt index 56d904b8dfe..9c9a3684ec4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModel.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.debug -import android.net.Uri import androidx.annotation.VisibleForTesting import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText @@ -32,16 +31,12 @@ import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.ui.home.settings.backup.BackupAndRestoreState import com.wire.android.ui.home.settings.backup.BackupCreationProgress -import com.wire.android.util.FileManager import com.wire.android.util.dispatchers.DefaultDispatcherProvider import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.feature.backup.CreateBackupResult import com.wire.kalium.logic.feature.backup.CreateObfuscatedCopyUseCase import com.wire.kalium.util.DelicateKaliumApi -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import javax.inject.Inject @ViewModelScopedPreview interface ExportObfuscatedCopyViewModel { @@ -53,15 +48,14 @@ interface ExportObfuscatedCopyViewModel { fun createObfuscatedCopy() {} fun shareCopy() {} - fun saveCopy(uri: Uri) {} + fun saveCopy(destinationUri: String) {} fun cancelBackupCreation() {} } -@HiltViewModel -class ExportObfuscatedCopyViewModelImpl @OptIn(DelicateKaliumApi::class) @Inject constructor( +class ExportObfuscatedCopyViewModelImpl @OptIn(DelicateKaliumApi::class) constructor( private val createUnencryptedCopy: CreateObfuscatedCopyUseCase, private val dispatcher: DispatcherProvider = DefaultDispatcherProvider(), - private val fileManager: FileManager, + private val fileGateway: ExportObfuscatedCopyFileGateway, ) : ViewModel(), ExportObfuscatedCopyViewModel { override var state by mutableStateOf(BackupAndRestoreState.INITIAL_STATE) @@ -97,9 +91,7 @@ class ExportObfuscatedCopyViewModelImpl @OptIn(DelicateKaliumApi::class) @Inject override fun shareCopy() { viewModelScope.launch { latestCreatedBackup?.let { backupData -> - withContext(dispatcher.io()) { - fileManager.shareWithExternalApp(backupData.path, backupData.assetName) {} - } + fileGateway.shareCopy(backupData.path, backupData.assetName) } state = state.copy( backupCreationProgress = BackupCreationProgress.InProgress(), @@ -107,10 +99,10 @@ class ExportObfuscatedCopyViewModelImpl @OptIn(DelicateKaliumApi::class) @Inject } } - override fun saveCopy(uri: Uri) { + override fun saveCopy(destinationUri: String) { viewModelScope.launch { latestCreatedBackup?.let { backupData -> - fileManager.copyToUri(backupData.path, uri, dispatcher) + fileGateway.saveCopy(backupData.path, destinationUri) } state = state.copy( backupCreationProgress = BackupCreationProgress.InProgress(), diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModelFactory.kt new file mode 100644 index 00000000000..f32a8fa6260 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModelFactory.kt @@ -0,0 +1,37 @@ +/* + * 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.debug + +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.backup.CreateObfuscatedCopyUseCase +import com.wire.kalium.util.DelicateKaliumApi +import dev.zacsweers.metro.Inject + +@Inject +class ExportObfuscatedCopyViewModelFactory( + private val createUnencryptedCopy: CreateObfuscatedCopyUseCase, + private val dispatcher: DispatcherProvider, + private val fileGateway: ExportObfuscatedCopyFileGateway, +) { + @OptIn(DelicateKaliumApi::class) + fun create(): ExportObfuscatedCopyViewModelImpl = ExportObfuscatedCopyViewModelImpl( + createUnencryptedCopy = createUnencryptedCopy, + dispatcher = dispatcher, + fileGateway = fileGateway, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementScreen.kt index f07a839b6c3..97ff74cffe2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementScreen.kt @@ -25,8 +25,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.scaffold.WireScaffold @@ -38,7 +38,9 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar fun LogManagementScreen( navigator: Navigator, modifier: Modifier = Modifier, - viewModel: LogManagementViewModel = hiltViewModel() + viewModel: LogManagementViewModel = metroViewModel { + logManagementViewModelFactory.create() + }, ) { val state = viewModel.state val contentState = rememberDebugContentState(state.logPath) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementViewModel.kt index 69fe1bf7c8f..1b211c60aa0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementViewModel.kt @@ -26,19 +26,16 @@ import com.wire.android.datastore.GlobalDataStore import com.wire.android.util.logging.LogFileWriter import com.wire.kalium.common.logger.CoreLogger import com.wire.kalium.logger.KaliumLogLevel -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.launch -import javax.inject.Inject data class LogManagementState( val isLoggingEnabled: Boolean = false, val logPath: String ) -@HiltViewModel -class LogManagementViewModel @Inject constructor( +class LogManagementViewModel( private val logFileWriter: LogFileWriter, private val globalDataStore: GlobalDataStore ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementViewModelFactory.kt new file mode 100644 index 00000000000..e09e7db13d2 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/LogManagementViewModelFactory.kt @@ -0,0 +1,33 @@ +/* + * 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.debug + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.util.logging.LogFileWriter +import dev.zacsweers.metro.Inject + +@Inject +class LogManagementViewModelFactory( + private val logFileWriter: LogFileWriter, + private val globalDataStore: GlobalDataStore, +) { + fun create(): LogManagementViewModel = LogManagementViewModel( + logFileWriter = logFileWriter, + globalDataStore = globalDataStore, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt b/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt index 93dd70ce2ce..d500dce3f0b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/StartServiceReceiver.kt @@ -22,32 +22,23 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.wire.android.appLogger -import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase -import com.wire.android.util.dispatchers.DispatcherProvider -import dagger.hilt.android.AndroidEntryPoint +import com.wire.android.notification.broadcastreceivers.broadcastReceiverDependencies import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import javax.inject.Inject /** * This BroadcastReceiver will restart the persistentWebSocket Service after restarting the device. */ -@AndroidEntryPoint class StartServiceReceiver : BroadcastReceiver() { - @Inject - lateinit var dispatcherProvider: DispatcherProvider - - @Inject - lateinit var startPersistentWebSocketService: StartPersistentWebsocketIfNecessaryUseCase - - private val scope by lazy { - CoroutineScope(SupervisorJob() + dispatcherProvider.io()) - } - override fun onReceive(context: Context?, intent: Intent?) { appLogger.i("$TAG: onReceive called with action ${intent?.action}") - scope.launch { startPersistentWebSocketService() } + context ?: return + + val dependencies = context.broadcastReceiverDependencies + CoroutineScope(SupervisorJob() + dependencies.dispatcherProvider().io()).launch { + dependencies.startPersistentWebsocketIfNecessary()() + } } companion object { diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModel.kt index 7fb4c3b7950..976de747e20 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModel.kt @@ -33,11 +33,9 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase import com.wire.kalium.logic.feature.debug.ChangeProfilingUseCase import com.wire.kalium.logic.feature.debug.ObserveDatabaseLoggerStateUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.launch -import javax.inject.Inject data class UserDebugState( val isLoggingEnabled: Boolean = false, @@ -50,9 +48,7 @@ data class UserDebugState( ) @Suppress("LongParameterList") -@HiltViewModel -class UserDebugViewModel -@Inject constructor( +class UserDebugViewModel( @CurrentAccount val currentAccount: UserId, private val logFileWriter: LogFileWriter, private val currentClientIdUseCase: ObserveCurrentClientIdUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModelFactory.kt new file mode 100644 index 00000000000..6ca76710f69 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModelFactory.kt @@ -0,0 +1,46 @@ +/* + * 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.debug + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.di.CurrentAccount +import com.wire.android.util.logging.LogFileWriter +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase +import com.wire.kalium.logic.feature.debug.ChangeProfilingUseCase +import com.wire.kalium.logic.feature.debug.ObserveDatabaseLoggerStateUseCase +import dev.zacsweers.metro.Inject + +@Inject +class UserDebugViewModelFactory( + @CurrentAccount private val currentAccount: UserId, + private val logFileWriter: LogFileWriter, + private val currentClientIdUseCase: ObserveCurrentClientIdUseCase, + private val globalDataStore: GlobalDataStore, + private val changeProfilingUseCase: ChangeProfilingUseCase, + private val observeDatabaseLoggerState: ObserveDatabaseLoggerStateUseCase, +) { + fun create(): UserDebugViewModel = UserDebugViewModel( + currentAccount = currentAccount, + logFileWriter = logFileWriter, + currentClientIdUseCase = currentClientIdUseCase, + globalDataStore = globalDataStore, + changeProfilingUseCase = changeProfilingUseCase, + observeDatabaseLoggerState = observeDatabaseLoggerState, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationScreen.kt index a4311592c63..87a1a999cbc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationScreen.kt @@ -35,9 +35,9 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.BuildConfig import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.Clickable import com.wire.android.navigation.Navigator import com.wire.android.ui.common.HandleActions @@ -65,8 +65,10 @@ import com.wire.kalium.logic.data.id.ConversationId @Composable fun DebugConversationScreen( navigator: Navigator, + args: DebugConversationScreenNavArgs, modifier: Modifier = Modifier, - viewModel: DebugConversationViewModel = hiltViewModel(), + viewModel: DebugConversationViewModel = + metroViewModel { debugConversationViewModelFactory.create(args) }, ) { val context = LocalContext.current diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModel.kt index 26860aedf2b..31203630442 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModel.kt @@ -17,11 +17,9 @@ */ package com.wire.android.ui.debug.conversation -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.ui.common.ActionsViewModel -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess import com.wire.kalium.logic.data.conversation.Conversation @@ -33,25 +31,20 @@ import com.wire.kalium.logic.feature.debug.DebugFeedConversationUseCase import com.wire.kalium.logic.feature.debug.DebugFeedResult import com.wire.kalium.logic.feature.debug.GetConversationEpochFromCCResult import com.wire.kalium.logic.feature.debug.GetConversationEpochFromCCUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class DebugConversationViewModel @Inject constructor( +class DebugConversationViewModel( private val conversationDetails: ObserveConversationDetailsUseCase, private val resetMLSConversation: ResetMLSConversationUseCase, private val fetchConversation: FetchConversationUseCase, private val feedConversation: DebugFeedConversationUseCase, private val getConversationEpochFromCC: GetConversationEpochFromCCUseCase, - savedStateHandle: SavedStateHandle, + args: DebugConversationScreenNavArgs, ) : ActionsViewModel() { - val args: DebugConversationScreenNavArgs = savedStateHandle.navArgs() - val conversationId = args.conversationId private val _state = MutableStateFlow(DebugConversationViewState()) diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModelFactory.kt new file mode 100644 index 00000000000..d2e8ac86e11 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModelFactory.kt @@ -0,0 +1,43 @@ +/* + * 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.debug.conversation + +import com.wire.kalium.logic.data.conversation.FetchConversationUseCase +import com.wire.kalium.logic.data.conversation.ResetMLSConversationUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.debug.DebugFeedConversationUseCase +import com.wire.kalium.logic.feature.debug.GetConversationEpochFromCCUseCase +import dev.zacsweers.metro.Inject + +@Inject +class DebugConversationViewModelFactory( + private val conversationDetails: ObserveConversationDetailsUseCase, + private val resetMLSConversation: ResetMLSConversationUseCase, + private val fetchConversation: FetchConversationUseCase, + private val feedConversation: DebugFeedConversationUseCase, + private val getConversationEpochFromCC: GetConversationEpochFromCCUseCase, +) { + fun create(args: DebugConversationScreenNavArgs): DebugConversationViewModel = DebugConversationViewModel( + conversationDetails = conversationDetails, + resetMLSConversation = resetMLSConversation, + fetchConversation = fetchConversation, + feedConversation = feedConversation, + getConversationEpochFromCC = getConversationEpochFromCC, + args = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsScreen.kt index 70398c8bb94..5facc6db4e6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsScreen.kt @@ -38,8 +38,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.ui.common.SearchBarInput import com.wire.android.ui.common.chip.WireFilterChip @@ -58,7 +58,9 @@ import com.wire.android.ui.theme.wireTypography fun ConversationCryptoStatsScreen( navigator: Navigator, modifier: Modifier = Modifier, - viewModel: ConversationCryptoStatsViewModel = hiltViewModel(), + viewModel: ConversationCryptoStatsViewModel = metroViewModel { + conversationCryptoStatsViewModelFactory.create() + }, ) { val scrollState = rememberScrollState() val state by viewModel.state.collectAsState() diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsViewModel.kt index 0fa1dda4f4b..5098ed47c75 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsViewModel.kt @@ -26,12 +26,10 @@ import com.wire.kalium.logic.feature.debug.ConversationCryptoStats import com.wire.kalium.logic.feature.debug.DetailGroupState import com.wire.kalium.logic.feature.debug.GetConversationCryptoStatsResult import com.wire.kalium.logic.feature.debug.GetConversationCryptoStatsUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject enum class ProtocolFilter(val label: String) { ALL("All"), @@ -49,8 +47,7 @@ enum class EstablishmentFilter(val label: String) { NOT_APPLICABLE("N/A (Proteus)"), } -@HiltViewModel -class ConversationCryptoStatsViewModel @Inject constructor( +class ConversationCryptoStatsViewModel( private val getConversationCryptoStats: GetConversationCryptoStatsUseCase, ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsViewModelFactory.kt new file mode 100644 index 00000000000..6c9f5378cfd --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.debug.cryptostats + +import com.wire.kalium.logic.feature.debug.GetConversationCryptoStatsUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ConversationCryptoStatsViewModelFactory( + private val getConversationCryptoStats: GetConversationCryptoStatsUseCase, +) { + fun create(): ConversationCryptoStatsViewModel = ConversationCryptoStatsViewModel( + getConversationCryptoStats = getConversationCryptoStats, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsScreen.kt index 892f09df10f..a405c2d55c8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsScreen.kt @@ -28,8 +28,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.ui.common.rememberTopBarElevationState import com.wire.android.ui.common.scaffold.WireScaffold @@ -43,7 +43,9 @@ import com.wire.android.ui.common.typography fun DebugFeatureFlagsScreen( navigator: Navigator, modifier: Modifier = Modifier, - viewModel: DebugFeatureFlagsViewModel = hiltViewModel(), + viewModel: DebugFeatureFlagsViewModel = metroViewModel { + debugFeatureFlagsViewModelFactory.create() + }, ) { val scrollState = rememberScrollState() diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsViewModel.kt index 0ccf0cd9bd6..7ac093b1b5a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsViewModel.kt @@ -24,16 +24,13 @@ import com.wire.kalium.logic.data.featureConfig.ChannelFeatureConfiguration import com.wire.kalium.logic.data.featureConfig.Status import com.wire.kalium.logic.feature.debug.GetFeatureConfigResult import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.serialization.json.Json -import javax.inject.Inject -@HiltViewModel -class DebugFeatureFlagsViewModel @Inject constructor( +class DebugFeatureFlagsViewModel( private val getFeatureConfig: GetFeatureConfigUseCase, ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsViewModelFactory.kt new file mode 100644 index 00000000000..2fa6c03c0de --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.debug.featureflags + +import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase +import dev.zacsweers.metro.Inject + +@Inject +class DebugFeatureFlagsViewModelFactory( + private val getFeatureConfig: GetFeatureConfigUseCase, +) { + fun create(): DebugFeatureFlagsViewModel = DebugFeatureFlagsViewModel( + getFeatureConfig = getFeatureConfig, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt index 45a414b1af3..c2a16718f6b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.NavigationSwitchAccountActions import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.LoginTypeSelector @@ -70,8 +70,8 @@ import com.wire.kalium.logic.feature.e2ei.usecase.FinalizeEnrollmentResult fun E2EIEnrollmentScreen( navigator: Navigator, loginTypeSelector: LoginTypeSelector, - viewModel: E2EIEnrollmentViewModel = hiltViewModel(), - clearSessionViewModel: ClearSessionViewModel = hiltViewModel(), + viewModel: E2EIEnrollmentViewModel = metroViewModel { e2EIEnrollmentViewModelFactory.create() }, + clearSessionViewModel: ClearSessionViewModel = metroViewModel { clearSessionViewModelFactory.create() }, ) { val state = viewModel.state diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt index e1ae5ecbbb3..3cb6e21f2a1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt @@ -24,9 +24,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment import com.wire.kalium.logic.feature.e2ei.usecase.FinalizeEnrollmentResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject data class E2EIEnrollmentState( val certificate: String = "null", @@ -37,8 +35,7 @@ data class E2EIEnrollmentState( val startGettingE2EICertificate: Boolean = false ) -@HiltViewModel -class E2EIEnrollmentViewModel @Inject constructor( +class E2EIEnrollmentViewModel( private val finalizeMLSClientAfterE2EIEnrollment: FinalizeMLSClientAfterE2EIEnrollment, ) : ViewModel() { var state by mutableStateOf(E2EIEnrollmentState()) diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModelFactory.kt new file mode 100644 index 00000000000..4419089ddb6 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.e2eiEnrollment + +import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment +import dev.zacsweers.metro.Inject + +@Inject +class E2EIEnrollmentViewModelFactory( + private val finalizeMLSClientAfterE2EIEnrollment: FinalizeMLSClientAfterE2EIEnrollment, +) { + fun create(): E2EIEnrollmentViewModel = E2EIEnrollmentViewModel( + finalizeMLSClientAfterE2EIEnrollment = finalizeMLSClientAfterE2EIEnrollment, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt index e4676f2ace7..0899b0a2ff4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateUI.kt @@ -21,7 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.e2ei.OAuthUseCase import com.wire.android.util.extension.getActivity import com.wire.kalium.logic.feature.e2ei.usecase.FinalizeEnrollmentResult @@ -32,7 +32,9 @@ import kotlinx.coroutines.flow.onEach fun GetE2EICertificateUI( enrollmentResultHandler: (FinalizeEnrollmentResult) -> Unit, isNewClient: Boolean, - viewModel: GetE2EICertificateViewModel = hiltViewModel() + viewModel: GetE2EICertificateViewModel = metroViewModel { + getE2EICertificateViewModelFactory.create() + } ) { val coroutineScope = rememberCoroutineScope() val context = LocalContext.current diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt index 4fc07d9c11d..508aae87313 100644 --- a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModel.kt @@ -27,15 +27,12 @@ import com.wire.kalium.logic.feature.e2ei.usecase.FinalizeEnrollmentResult import com.wire.kalium.logic.feature.e2ei.usecase.InitialEnrollmentResult import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class GetE2EICertificateViewModel @Inject constructor( +class GetE2EICertificateViewModel( @KaliumCoreLogic private val coreLogic: CoreLogic, private val currentSession: CurrentSessionUseCase, val dispatcherProvider: DispatcherProvider diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModelFactory.kt new file mode 100644 index 00000000000..be43fe3c2ab --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModelFactory.kt @@ -0,0 +1,37 @@ +/* + * 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.e2eiEnrollment + +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import dev.zacsweers.metro.Inject + +@Inject +class GetE2EICertificateViewModelFactory( + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val currentSession: CurrentSessionUseCase, + private val dispatcherProvider: DispatcherProvider, +) { + fun create(): GetE2EICertificateViewModel = GetE2EICertificateViewModel( + coreLogic = coreLogic, + currentSession = currentSession, + dispatcherProvider = dispatcherProvider, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt index a9bc143870a..4ca5974797f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt @@ -22,17 +22,14 @@ import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.sync.ForegroundActionsUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes -@HiltViewModel -class AppSyncViewModel @Inject constructor( +class AppSyncViewModel( private val foregroundActionsUseCase: ForegroundActionsUseCase, private val dispatcher: DispatcherProvider, ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModelFactory.kt new file mode 100644 index 00000000000..bbff7ee894a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModelFactory.kt @@ -0,0 +1,33 @@ +/* + * 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 + +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.sync.ForegroundActionsUseCase +import dev.zacsweers.metro.Inject + +@Inject +class AppSyncViewModelFactory( + private val foregroundActionsUseCase: ForegroundActionsUseCase, + private val dispatcher: DispatcherProvider, +) { + fun create(): AppSyncViewModel = AppSyncViewModel( + foregroundActionsUseCase = foregroundActionsUseCase, + dispatcher = dispatcher, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt index eaa00a1ee51..4169e2fd11d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt @@ -57,26 +57,27 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.generated.app.destinations.ConversationFoldersScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.GlobalCellsScreenDestination +import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.NewConversationSearchPeopleScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.SelfUserProfileScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.GlobalCellsScreenDestination -import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination import com.ramcosta.composedestinations.generated.app.navgraphs.HomeGraph import com.ramcosta.composedestinations.navigation.dependency import com.ramcosta.composedestinations.navigation.destination -import com.wire.android.feature.cells.ui.CellViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import com.wire.android.R import com.wire.android.appLogger +import com.wire.android.di.metro.metroViewModel +import com.wire.android.feature.cells.ui.CellFilesNavArgs +import com.wire.android.feature.cells.ui.CellViewModel import com.wire.android.navigation.HomeDestination import com.wire.android.navigation.HomeDestination.FabOptions import com.wire.android.navigation.NavigationCommand @@ -112,10 +113,10 @@ fun HomeScreen( otherUserProfileScreenResultRecipient: ResultRecipient, conversationFoldersScreenResultRecipient: ResultRecipient, - homeViewModel: HomeViewModel = hiltViewModel(), - appSyncViewModel: AppSyncViewModel = hiltViewModel(), - homeDrawerViewModel: HomeDrawerViewModel = hiltViewModel(), - analyticsUsageViewModel: AnalyticsUsageViewModel = hiltViewModel(), + homeViewModel: HomeViewModel = metroViewModel { homeViewModelFactory.create() }, + appSyncViewModel: AppSyncViewModel = metroViewModel { appSyncViewModelFactory.create() }, + homeDrawerViewModel: HomeDrawerViewModel = metroViewModel { homeDrawerViewModelFactory.create() }, + analyticsUsageViewModel: AnalyticsUsageViewModel = metroViewModel { analyticsUsageViewModelFactory.create() }, ) { val context = LocalContext.current @@ -159,7 +160,7 @@ fun HomeScreen( } } - LaunchedEffect(homeViewModel.savedStateHandle) { + LaunchedEffect(Unit) { showNotificationsFlow.launch() } @@ -376,7 +377,11 @@ fun HomeContent( homeStateHolder.navigator.navController .getBackStackEntry(HomeScreenDestination.route) } - dependency(hiltViewModel(parentEntry)) + dependency( + metroViewModel(viewModelStoreOwner = parentEntry) { + cellViewModelFactory.create(CellFilesNavArgs(), null) + } + ) } } ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt index bf187da1c6c..4030cf4cbf8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt @@ -22,7 +22,6 @@ import androidx.annotation.VisibleForTesting import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.datastore.UserDataStore import com.wire.android.model.ImageAsset.UserAvatarAsset @@ -39,18 +38,14 @@ import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import dagger.Lazy -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("LongParameterList") -@HiltViewModel -class HomeViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class HomeViewModel( private val dataStore: UserDataStore, private val observeSelf: ObserveSelfUserUseCase, private val needsToRegisterClient: NeedsToRegisterClientUseCase, 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 new file mode 100644 index 00000000000..32a3b1ad5d5 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModelFactory.kt @@ -0,0 +1,46 @@ +/* + * 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 + +import com.wire.android.datastore.UserDataStore +import com.wire.kalium.logic.feature.client.NeedsToRegisterClientUseCase +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import dagger.Lazy +import dev.zacsweers.metro.Inject + +@Inject +class HomeViewModelFactory( + private val dataStore: UserDataStore, + private val observeSelf: ObserveSelfUserUseCase, + private val needsToRegisterClient: NeedsToRegisterClientUseCase, + private val canMigrateFromPersonalToTeam: CanMigrateFromPersonalToTeamUseCase, + private val observeLegalHoldStatusForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, + private val currentSessionFlow: Lazy, +) { + fun create(): HomeViewModel = HomeViewModel( + dataStore = dataStore, + observeSelf = observeSelf, + needsToRegisterClient = needsToRegisterClient, + canMigrateFromPersonalToTeam = canMigrateFromPersonalToTeam, + observeLegalHoldStatusForSelfUser = observeLegalHoldStatusForSelfUser, + currentSessionFlow = currentSessionFlow, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt index 06681bcb0f8..4ac93be408a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt @@ -128,4 +128,6 @@ class LockCodeTimeManager @Inject constructor( } } -typealias CurrentTimestampProvider = () -> Long +fun interface CurrentTimestampProvider { + operator fun invoke(): Long +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt index 06a1bd319ae..61e876cf1ce 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt @@ -47,8 +47,8 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.style.TextAlign -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.LocalActivity import com.wire.android.ui.WireActivity import com.wire.android.ui.common.WireDialog @@ -71,7 +71,9 @@ import com.wire.android.util.ui.PreviewMultipleThemes @WireRootDestination @Composable fun ForgotLockCodeScreen( - viewModel: ForgotLockScreenViewModel = hiltViewModel(), + viewModel: ForgotLockScreenViewModel = metroViewModel { + forgotLockScreenViewModelFactory.create() + }, ) { val activity = LocalActivity.current val logoutOptionsDialogState = rememberVisibilityState() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt index 635a9505a29..f7ec34a09cc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModel.kt @@ -36,15 +36,12 @@ import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.session.GetAllSessionsResult import com.wire.kalium.logic.feature.session.GetSessionsUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("LongParameterList") -@HiltViewModel -class ForgotLockScreenViewModel @Inject constructor( +class ForgotLockScreenViewModel( @KaliumCoreLogic private val coreLogic: CoreLogic, private val globalDataStore: GlobalDataStore, private val notificationManager: WireNotificationManager, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelFactory.kt new file mode 100644 index 00000000000..d6bb40ef429 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelFactory.kt @@ -0,0 +1,53 @@ +/* + * 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.appLock.forgot + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.datastore.UserDataStoreProvider +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.notification.WireNotificationManager +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.session.GetSessionsUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class ForgotLockScreenViewModelFactory( + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val globalDataStore: GlobalDataStore, + private val notificationManager: WireNotificationManager, + private val userDataStoreProvider: UserDataStoreProvider, + private val getSessions: GetSessionsUseCase, + private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, + private val endCall: EndCallUseCase, + private val accountSwitch: AccountSwitchUseCase, +) { + fun create(): ForgotLockScreenViewModel = ForgotLockScreenViewModel( + coreLogic = coreLogic, + globalDataStore = globalDataStore, + notificationManager = notificationManager, + userDataStoreProvider = userDataStoreProvider, + getSessions = getSessions, + observeEstablishedCalls = observeEstablishedCalls, + endCall = endCall, + accountSwitch = accountSwitch, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeScreen.kt index 318987a2b50..2b75f79bc57 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockCodeScreen.kt @@ -49,8 +49,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.input.ImeAction -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.navigation.rememberNavigator import com.wire.android.ui.common.button.WireButtonState @@ -78,7 +78,7 @@ import java.util.Locale @Composable fun SetLockCodeScreen( navigator: Navigator, - viewModel: SetLockScreenViewModel = hiltViewModel(), + viewModel: SetLockScreenViewModel = metroViewModel { setLockScreenViewModelFactory.create() }, ) { SetLockCodeScreenContent( navigator = navigator, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt index dbd8927ebac..1eb28e3a6c4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModel.kt @@ -31,15 +31,12 @@ import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel -class SetLockScreenViewModel @Inject constructor( +class SetLockScreenViewModel( private val validatePassword: ValidatePasswordUseCase, private val globalDataStore: GlobalDataStore, private val dispatchers: DispatcherProvider, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModelFactory.kt new file mode 100644 index 00000000000..f2644dcc94a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModelFactory.kt @@ -0,0 +1,45 @@ +/* + * 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.appLock.set + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.feature.ObserveAppLockConfigUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase +import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase +import dev.zacsweers.metro.Inject + +@Inject +class SetLockScreenViewModelFactory( + private val validatePassword: ValidatePasswordUseCase, + private val globalDataStore: GlobalDataStore, + private val dispatchers: DispatcherProvider, + private val observeAppLockConfig: ObserveAppLockConfigUseCase, + private val observeIsAppLockEditable: ObserveIsAppLockEditableUseCase, + private val markTeamAppLockStatusAsNotified: MarkTeamAppLockStatusAsNotifiedUseCase, +) { + fun create(): SetLockScreenViewModel = SetLockScreenViewModel( + validatePassword = validatePassword, + globalDataStore = globalDataStore, + dispatchers = dispatchers, + observeAppLockConfig = observeAppLockConfig, + observeIsAppLockEditable = observeIsAppLockEditable, + markTeamAppLockStatusAsNotified = markTeamAppLockStatusAsNotified, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsScreen.kt index 1fe307570d7..945a5db3153 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsScreen.kt @@ -34,10 +34,10 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R import com.wire.android.appLogger import com.wire.android.biometric.showBiometricPrompt +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -49,7 +49,9 @@ import com.ramcosta.composedestinations.generated.app.destinations.EnterLockCode @Composable fun AppUnlockWithBiometricsScreen( navigator: Navigator, - appUnlockWithBiometricsViewModel: AppUnlockWithBiometricsViewModel = hiltViewModel() + appUnlockWithBiometricsViewModel: AppUnlockWithBiometricsViewModel = metroViewModel { + appUnlockWithBiometricsViewModelFactory.create() + } ) { AppUnLockBackground() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsViewModel.kt index e62d7a04d81..3a0c1a016a1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsViewModel.kt @@ -19,11 +19,8 @@ package com.wire.android.ui.home.appLock.unlock import androidx.lifecycle.ViewModel import com.wire.android.ui.home.appLock.LockCodeTimeManager -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -@HiltViewModel -class AppUnlockWithBiometricsViewModel @Inject constructor( +class AppUnlockWithBiometricsViewModel( private val lockCodeTimeManager: LockCodeTimeManager ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsViewModelFactory.kt new file mode 100644 index 00000000000..cd5a15f2914 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.appLock.unlock + +import com.wire.android.ui.home.appLock.LockCodeTimeManager +import dev.zacsweers.metro.Inject + +@Inject +class AppUnlockWithBiometricsViewModelFactory( + private val lockCodeTimeManager: LockCodeTimeManager, +) { + fun create(): AppUnlockWithBiometricsViewModel = AppUnlockWithBiometricsViewModel( + lockCodeTimeManager = lockCodeTimeManager, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt index eb655da22c5..3860a76d32a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt @@ -48,9 +48,9 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.input.ImeAction -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.utils.destination import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.common.button.WireButtonState @@ -74,7 +74,9 @@ import java.util.Locale @Composable fun EnterLockCodeScreen( navigator: Navigator, - viewModel: EnterLockScreenViewModel = hiltViewModel(), + viewModel: EnterLockScreenViewModel = metroViewModel { + enterLockScreenViewModelFactory.create() + }, ) { EnterLockCodeScreenContent( state = viewModel.state, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModel.kt index 25f1bc2a37a..6bb44cf3f1a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModel.kt @@ -29,15 +29,12 @@ import com.wire.android.ui.home.appLock.LockCodeTimeManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.sha256 import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel -class EnterLockScreenViewModel @Inject constructor( +class EnterLockScreenViewModel( private val validatePassword: ValidatePasswordUseCase, private val globalDataStore: GlobalDataStore, private val dispatchers: DispatcherProvider, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModelFactory.kt new file mode 100644 index 00000000000..b78a8e29c90 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModelFactory.kt @@ -0,0 +1,39 @@ +/* + * 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.appLock.unlock + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.ui.home.appLock.LockCodeTimeManager +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase +import dev.zacsweers.metro.Inject + +@Inject +class EnterLockScreenViewModelFactory( + private val validatePassword: ValidatePasswordUseCase, + private val globalDataStore: GlobalDataStore, + private val dispatchers: DispatcherProvider, + private val lockCodeTimeManager: LockCodeTimeManager, +) { + fun create(): EnterLockScreenViewModel = EnterLockScreenViewModel( + validatePassword = validatePassword, + globalDataStore = globalDataStore, + dispatchers = dispatchers, + lockCodeTimeManager = lockCodeTimeManager, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModel.kt index f6f3b59585a..80d75d47461 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModel.kt @@ -21,20 +21,13 @@ import androidx.annotation.VisibleForTesting import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs -import com.wire.android.di.AssistedViewModelFactory import com.wire.android.di.ViewModelScopedPreview import com.wire.android.ui.home.conversations.model.CompositeMessageArgs import com.wire.kalium.logic.data.id.MessageButtonId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @ViewModelScopedPreview @@ -44,15 +37,12 @@ interface CompositeMessageViewModel { fun sendButtonActionMessage(buttonId: String) {} } -@HiltViewModel(assistedFactory = CompositeMessageViewModelImpl.Factory::class) -class CompositeMessageViewModelImpl @AssistedInject constructor( +class CompositeMessageViewModelImpl( private val sendButtonActionMessageUseCase: SendButtonActionMessageUseCase, - savedStateHandle: SavedStateHandle, - @Assisted private val scopedArgs: CompositeMessageArgs, + private val scopedArgs: CompositeMessageArgs, ) : CompositeMessageViewModel, ViewModel() { - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() - val conversationId: QualifiedID = conversationNavArgs.conversationId + val conversationId: QualifiedID = scopedArgs.conversationId private val messageId: String = scopedArgs.messageId override var pendingButtonId: MessageButtonId? by mutableStateOf(null) @@ -69,9 +59,4 @@ class CompositeMessageViewModelImpl @AssistedInject constructor( pendingButtonId = null } } - - @AssistedFactory - interface Factory : AssistedViewModelFactory { - override fun create(args: CompositeMessageArgs): CompositeMessageViewModelImpl - } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelFactory.kt new file mode 100644 index 00000000000..82c7ca31f3f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelFactory.kt @@ -0,0 +1,32 @@ +/* + * 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.conversations + +import com.wire.android.ui.home.conversations.model.CompositeMessageArgs +import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase +import dev.zacsweers.metro.Inject + +@Inject +class CompositeMessageViewModelFactory( + private val sendButtonActionMessageUseCase: SendButtonActionMessageUseCase, +) { + fun create(args: CompositeMessageArgs): CompositeMessageViewModelImpl = CompositeMessageViewModelImpl( + sendButtonActionMessageUseCase = sendButtonActionMessageUseCase, + scopedArgs = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 507195c12a8..c1b1b5f7684 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -20,7 +20,6 @@ package com.wire.android.ui.home.conversations import android.annotation.SuppressLint -import android.content.Context import android.net.Uri import android.text.format.DateUtils import androidx.activity.compose.BackHandler @@ -79,7 +78,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.core.net.toUri import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems @@ -102,6 +101,7 @@ import com.sebaslogen.resaca.rememberKeysInScope import com.wire.android.BuildConfig.IS_BUBBLE_UI_ENABLED import com.wire.android.R import com.wire.android.appLogger +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.feature.cells.ui.dialog.IncompatibleFileNameDialog @@ -248,21 +248,31 @@ private const val MAX_GROUP_SIZE_FOR_PING = 3 @Composable fun ConversationScreen( navigator: Navigator, + args: ConversationNavArgs, groupDetailsScreenResultRecipient: ResultRecipient, mediaGalleryScreenResultRecipient: ResultRecipient, imagePreviewScreenResultRecipient: ResultRecipient, drawingCanvasScreenResultRecipient: OpenResultRecipient, resultNavigator: ResultBackNavigator, - conversationInfoViewModel: ConversationInfoViewModel = hiltViewModel(), - conversationBannerViewModel: ConversationBannerViewModel = hiltViewModel(), - conversationCallViewModel: ConversationCallViewModel = hiltViewModel(), - conversationMessagesViewModel: ConversationMessagesViewModel = hiltViewModel(), - messageComposerViewModel: MessageComposerViewModel = hiltViewModel(), - sendMessageViewModel: SendMessageViewModel = hiltViewModel(), - conversationMigrationViewModel: ConversationMigrationViewModel = hiltViewModel(), - messageDraftViewModel: MessageDraftViewModel = hiltViewModel(), - messageAttachmentsViewModel: MessageAttachmentsViewModel = hiltViewModel(), + conversationInfoViewModel: ConversationInfoViewModel = + metroViewModel { conversationInfoViewModelFactory.create(args) }, + conversationBannerViewModel: ConversationBannerViewModel = + metroViewModel { conversationBannerViewModelFactory.create(args) }, + conversationCallViewModel: ConversationCallViewModel = + metroViewModel { conversationCallViewModelFactory.create(args) }, + conversationMessagesViewModel: ConversationMessagesViewModel = + metroViewModel { conversationMessagesViewModelFactory.create(args) }, + messageComposerViewModel: MessageComposerViewModel = + metroViewModel { messageComposerViewModelFactory.create(args) }, + sendMessageViewModel: SendMessageViewModel = + metroViewModel { sendMessageViewModelFactory.create(args) }, + conversationMigrationViewModel: ConversationMigrationViewModel = + metroViewModel { conversationMigrationViewModelFactory.create(args) }, + messageDraftViewModel: MessageDraftViewModel = + metroViewModel { messageDraftViewModelFactory.create(args) }, + messageAttachmentsViewModel: MessageAttachmentsViewModel = + metroViewModel { messageAttachmentsViewModelFactory.create(args) }, ) { val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current @@ -526,7 +536,7 @@ fun ConversationScreen( }, onImagesPicked = { it, fromKeyboard -> if (conversationInfoViewModel.conversationInfoViewState.isWireCellEnabled && !fromKeyboard) { - messageAttachmentsViewModel.onFilesSelected(it) + messageAttachmentsViewModel.onFilesSelected(it.map { uri -> uri.toString() }) messageComposerStateHolder.messageCompositionInputStateHolder.showAttachments(false) } else { navigator.navigate( @@ -542,7 +552,7 @@ fun ConversationScreen( }, onAttachmentPicked = { if (conversationInfoViewModel.conversationInfoViewState.isWireCellEnabled) { - messageAttachmentsViewModel.onFilesSelected(listOf(it.uri)) + messageAttachmentsViewModel.onFilesSelected(listOf(it.uri.toString())) messageComposerStateHolder.messageCompositionInputStateHolder.showAttachments(false) } else { val bundle = ComposableMessageBundle.UriPickedBundle(conversationInfoViewModel.conversationId, it) @@ -552,7 +562,7 @@ fun ConversationScreen( onAudioRecorded = { messageComposerStateHolder.messageCompositionInputStateHolder.showAttachments(false) if (conversationInfoViewModel.conversationInfoViewState.isWireCellEnabled) { - messageAttachmentsViewModel.onAudioRecorded(it.uri, it.audioWavesMask) + messageAttachmentsViewModel.onAudioRecorded(it.uri.toString(), it.audioWavesMask) } else { val bundle = ComposableMessageBundle.AudioMessageBundle(conversationInfoViewModel.conversationId, it) sendMessageViewModel.trySendMessage(bundle) @@ -625,8 +635,8 @@ fun ConversationScreen( onNavigateToReplyOriginalMessage = conversationMessagesViewModel::navigateToReplyOriginalMessage, onSelfDeletingMessageRead = messageComposerViewModel::startSelfDeletion, onNewSelfDeletingMessagesStatus = messageComposerViewModel::updateSelfDeletingMessages, - tempWritableImageUri = messageComposerViewModel.tempWritableImageUri, - tempWritableVideoUri = messageComposerViewModel.tempWritableVideoUri, + tempWritableImageUri = messageComposerViewModel.tempWritableImageUri?.toUri(), + tempWritableVideoUri = messageComposerViewModel.tempWritableVideoUri?.toUri(), onFailedMessageRetryClicked = sendMessageViewModel::retrySendingMessage, onClearMentionSearchResult = messageComposerViewModel::clearMentionSearchResult, onPermissionPermanentlyDenied = { @@ -666,7 +676,7 @@ fun ConversationScreen( DrawingCanvasScreenDestination( DrawingCanvasNavArgs( conversationName = conversationInfoViewModel.conversationInfoViewState.conversationName.asString(resources), - tempWritableUri = messageComposerViewModel.tempWritableImageUri + tempWritableUri = messageComposerViewModel.tempWritableImageUri?.toUri() ) ) ) @@ -934,7 +944,7 @@ private fun ConversationScreen( onBackButtonClick: () -> Unit, composerMessages: SharedFlow, conversationMessages: SharedFlow, - shareAsset: (Context, messageId: String) -> Unit, + shareAsset: (messageId: String) -> Unit, onDownloadAssetClick: (messageId: String) -> Unit, onOpenAssetClick: (messageId: String) -> Unit, onNavigateToReplyOriginalMessage: (UIMessage) -> Unit, @@ -957,7 +967,6 @@ private fun ConversationScreen( hasMoreRemoteMessages: Boolean = false, isWireCellsEnabled: Boolean = false, ) { - val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current Box(modifier = Modifier) { // only here we will use normal Scaffold because of specific behaviour of message composer @@ -1075,7 +1084,7 @@ private fun ConversationScreen( onDetailsClick = onMessageDetailsClick, onReplyClick = messageComposerStateHolder::toReply, onEditClick = messageComposerStateHolder::toEdit, - onShareAssetClick = { shareAsset(context, it) }, + onShareAssetClick = shareAsset, onDownloadAssetClick = onDownloadAssetClick, onOpenAssetClick = onOpenAssetClick, ) @@ -1910,7 +1919,7 @@ fun PreviewConversationScreen() = WireTheme { onBackButtonClick = {}, composerMessages = MutableStateFlow(ConversationSnackbarMessages.ErrorDownloadingAsset), conversationMessages = MutableStateFlow(ConversationSnackbarMessages.ErrorDownloadingAsset), - shareAsset = { _, _ -> }, + shareAsset = { }, onOpenAssetClick = {}, onDownloadAssetClick = {}, onNavigateToReplyOriginalMessage = {}, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageSharedState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageSharedState.kt index d03403f6c93..633b54568da 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageSharedState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageSharedState.kt @@ -20,7 +20,7 @@ package com.wire.android.ui.home.conversations import com.wire.android.ui.home.conversations.model.AssetBundle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import javax.inject.Inject +import dev.zacsweers.metro.Inject /** * WARNING: diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/UsersTypingIndicator.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/UsersTypingIndicator.kt index 5c784ed3621..a45404e4edc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/UsersTypingIndicator.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/UsersTypingIndicator.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.model.NameBasedAvatar import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.avatar.UserProfileAvatarsRow @@ -58,6 +58,7 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant import com.wire.android.ui.home.conversations.typing.TypingIndicatorArgs import com.wire.android.ui.home.conversations.typing.TypingIndicatorViewModel +import com.wire.android.ui.home.conversations.typing.TypingIndicatorViewModelFactory import com.wire.android.ui.home.conversations.typing.TypingIndicatorViewModelImpl import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.theme.WireTheme @@ -74,14 +75,12 @@ private const val ANIMATION_SPEED_MILLIS = 1_500 fun UsersTypingIndicatorForConversation( conversationId: ConversationId, viewModel: TypingIndicatorViewModel = - hiltViewModelScoped< + wireViewModelScoped< TypingIndicatorViewModelImpl, TypingIndicatorViewModel, TypingIndicatorArgs, - TypingIndicatorViewModelImpl.Factory - >( - TypingIndicatorArgs(conversationId) - ) + TypingIndicatorViewModelFactory, + >(TypingIndicatorArgs(conversationId)) ) { UsersTypingIndicator(usersTyping = viewModel.state().usersTyping) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentAssetImporter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentAssetImporter.kt new file mode 100644 index 00000000000..8cbfd3f7ed2 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentAssetImporter.kt @@ -0,0 +1,38 @@ +/* + * 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.conversations.attachment + +import androidx.core.net.toUri +import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase +import com.wire.android.ui.sharing.ImportedMediaAsset + +interface MessageAttachmentAssetImporter { + suspend fun importAsset(uri: String): ImportedMediaAsset? +} + +class MessageAttachmentAssetImporterImpl( + private val handleUriAsset: HandleUriAssetUseCase, +) : MessageAttachmentAssetImporter { + + override suspend fun importAsset(uri: String): ImportedMediaAsset? = + when (val result = handleUriAsset.invoke(uri.toUri(), saveToDeviceIfInvalid = false)) { + is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> ImportedMediaAsset(result.assetBundle, result.maxLimitInMB) + is HandleUriAssetUseCase.Result.Success -> ImportedMediaAsset(result.assetBundle, null) + is HandleUriAssetUseCase.Result.Failure.Unknown -> null + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentFileGateway.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentFileGateway.kt new file mode 100644 index 00000000000..0aeb45f4c57 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentFileGateway.kt @@ -0,0 +1,56 @@ +/* + * 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.conversations.attachment + +import android.webkit.MimeTypeMap +import com.wire.android.media.audiomessage.toNormalizedLoudness +import com.wire.android.util.FileManager +import com.wire.android.util.getAudioLengthInMs +import com.wire.kalium.logic.data.message.AssetContent +import com.wire.kalium.logic.util.fileExtension +import okio.Path +import okio.Path.Companion.toPath +import java.io.File + +interface MessageAttachmentFileGateway { + fun exists(localFilePath: String): Boolean + fun open(localFilePath: String, fileName: String, onError: () -> Unit) + fun audioMetadata(dataPath: Path, mimeType: String, wavesMask: List?): AssetContent.AssetMetadata.Audio +} + +class MessageAttachmentFileGatewayImpl( + private val fileManager: FileManager, +) : MessageAttachmentFileGateway { + + override fun exists(localFilePath: String): Boolean = File(localFilePath).exists() + + override fun open(localFilePath: String, fileName: String, onError: () -> Unit) { + val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileName.fileExtension() ?: "") + fileManager.openWithExternalApp(localFilePath.toPath(), fileName, mimeType, onError) + } + + override fun audioMetadata( + dataPath: Path, + mimeType: String, + wavesMask: List?, + ): AssetContent.AssetMetadata.Audio = + AssetContent.AssetMetadata.Audio( + durationMs = getAudioLengthInMs(dataPath, mimeType), + normalizedLoudness = wavesMask?.toNormalizedLoudness(), + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentModule.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentModule.kt new file mode 100644 index 00000000000..3281f608f02 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentModule.kt @@ -0,0 +1,20 @@ +/* + * 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.conversations.attachment + +// Message attachment bindings are provided by the Metro graph. diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModel.kt index 0394b9def93..95115f6e845 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModel.kt @@ -17,28 +17,19 @@ */ package com.wire.android.ui.home.conversations.attachment -import android.net.Uri -import android.webkit.MimeTypeMap import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger -import com.wire.android.media.audiomessage.toNormalizedLoudness import com.wire.android.ui.common.attachmentdraft.model.AttachmentDraftUi import com.wire.android.ui.common.attachmentdraft.model.toUiModel import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.MessageSharedState import com.wire.android.ui.home.conversations.model.AssetBundle -import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase -import com.ramcosta.composedestinations.generated.app.navArgs -import com.wire.android.ui.sharing.ImportedMediaAsset -import com.wire.android.util.FileManager import com.wire.android.util.GetMediaMetadataUseCase -import com.wire.android.util.getAudioLengthInMs import com.wire.kalium.cells.domain.CellUploadEvent import com.wire.kalium.cells.domain.CellUploadManager import com.wire.kalium.cells.domain.model.AttachmentDraft @@ -50,9 +41,6 @@ import com.wire.kalium.cells.domain.usecase.RetryAttachmentUploadUseCase import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess import com.wire.kalium.logic.data.id.QualifiedID -import com.wire.kalium.logic.data.message.AssetContent -import com.wire.kalium.logic.util.fileExtension -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow @@ -60,26 +48,21 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import okio.Path.Companion.toPath -import java.io.File -import javax.inject.Inject @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel -class MessageAttachmentsViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, - private val handleUriAsset: HandleUriAssetUseCase, +class MessageAttachmentsViewModel( + conversationNavArgs: ConversationNavArgs, + private val assetImporter: MessageAttachmentAssetImporter, private val observeAttachments: ObserveAttachmentDraftsUseCase, private val addAttachment: AddAttachmentDraftUseCase, private val removeAttachment: RemoveAttachmentDraftUseCase, private val retryUpload: RetryAttachmentUploadUseCase, private val uploadManager: CellUploadManager, - private val fileManager: FileManager, + private val fileGateway: MessageAttachmentFileGateway, private val sharedState: MessageSharedState, private val getMediaMetadata: GetMediaMetadataUseCase, ) : ViewModel() { - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() private val conversationId: QualifiedID = conversationNavArgs.conversationId private val uploadObservers = mutableMapOf() private val removedAttachments = MutableStateFlow(emptyList()) @@ -113,27 +96,24 @@ class MessageAttachmentsViewModel @Inject constructor( } } - fun onAudioRecorded(uri: Uri, wavesMask: List?) = viewModelScope.launch { - handleImportedAsset(uri)?.assetBundle?.let { bundle -> + fun onAudioRecorded(uri: String, wavesMask: List?) = viewModelScope.launch { + assetImporter.importAsset(uri)?.assetBundle?.let { bundle -> addAttachment( conversationId = conversationId, fileName = bundle.fileName, assetPath = bundle.dataPath, assetSize = bundle.dataSize, mimeType = bundle.mimeType, - assetMetadata = AssetContent.AssetMetadata.Audio( - durationMs = getAudioLengthInMs(bundle.dataPath, bundle.mimeType), - normalizedLoudness = wavesMask?.toNormalizedLoudness() - ) + assetMetadata = fileGateway.audioMetadata(bundle.dataPath, bundle.mimeType, wavesMask) ).onFailure { appLogger.e("Failed to add recorded audio attachment: $it", tag = "MessageAttachmentsViewModel") } } } - fun onFilesSelected(uriList: List) = viewModelScope.launch { + fun onFilesSelected(uriList: List) = viewModelScope.launch { uriList.forEach { uri -> - handleImportedAsset(uri)?.let { asset -> + assetImporter.importAsset(uri)?.let { asset -> enqueueOrAddAttachment(asset.assetBundle) } } @@ -178,13 +158,6 @@ class MessageAttachmentsViewModel @Inject constructor( showNextIncompatibleDialog() } - private suspend fun handleImportedAsset(uri: Uri): ImportedMediaAsset? = - when (val result = handleUriAsset.invoke(uri, saveToDeviceIfInvalid = false)) { - is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> ImportedMediaAsset(result.assetBundle, result.maxLimitInMB) - is HandleUriAssetUseCase.Result.Success -> ImportedMediaAsset(result.assetBundle, null) - is HandleUriAssetUseCase.Result.Failure.Unknown -> null - } - private fun addAttachment(bundle: AssetBundle) = viewModelScope.launch { addAttachment( conversationId = conversationId, @@ -203,7 +176,7 @@ class MessageAttachmentsViewModel @Inject constructor( if (attachment.uploadError) { failedAttachmentDialogState = FailedAttachmentDialogState.Visible( attachment = attachment, - showRetryOption = File(attachment.localFilePath).exists(), + showRetryOption = fileGateway.exists(attachment.localFilePath), ) } else { deleteAttachment(attachment) @@ -264,7 +237,7 @@ class MessageAttachmentsViewModel @Inject constructor( if (attachment.uploadError) { failedAttachmentDialogState = FailedAttachmentDialogState.Visible( attachment = attachment, - showRetryOption = File(attachment.localFilePath).exists(), + showRetryOption = fileGateway.exists(attachment.localFilePath), ) } else { showAttachment(attachment) @@ -272,8 +245,7 @@ class MessageAttachmentsViewModel @Inject constructor( } private fun showAttachment(attachment: AttachmentDraftUi) { - val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(attachment.fileName.fileExtension() ?: "") - fileManager.openWithExternalApp(attachment.localFilePath.toPath(), attachment.fileName, mimeType) { + fileGateway.open(attachment.localFilePath, attachment.fileName) { appLogger.e("Failed to open: ${attachment.localFilePath}", tag = "MessageAttachmentsViewModel") } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModelFactory.kt new file mode 100644 index 00000000000..3bf25384b06 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModelFactory.kt @@ -0,0 +1,55 @@ +/* + * 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.conversations.attachment + +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.home.conversations.MessageSharedState +import com.wire.android.util.GetMediaMetadataUseCase +import com.wire.kalium.cells.domain.CellUploadManager +import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCase +import com.wire.kalium.cells.domain.usecase.ObserveAttachmentDraftsUseCase +import com.wire.kalium.cells.domain.usecase.RemoveAttachmentDraftUseCase +import com.wire.kalium.cells.domain.usecase.RetryAttachmentUploadUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class MessageAttachmentsViewModelFactory( + private val assetImporter: MessageAttachmentAssetImporter, + private val observeAttachments: ObserveAttachmentDraftsUseCase, + private val addAttachment: AddAttachmentDraftUseCase, + private val removeAttachment: RemoveAttachmentDraftUseCase, + private val retryUpload: RetryAttachmentUploadUseCase, + private val uploadManager: CellUploadManager, + private val fileGateway: MessageAttachmentFileGateway, + private val sharedState: MessageSharedState, + private val getMediaMetadata: GetMediaMetadataUseCase, +) { + fun create(args: ConversationNavArgs): MessageAttachmentsViewModel = MessageAttachmentsViewModel( + conversationNavArgs = args, + assetImporter = assetImporter, + observeAttachments = observeAttachments, + addAttachment = addAttachment, + removeAttachment = removeAttachment, + retryUpload = retryUpload, + uploadManager = uploadManager, + fileGateway = fileGateway, + sharedState = sharedState, + getMediaMetadata = getMediaMetadata, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt index d193e835770..5fbfc744f2b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModel.kt @@ -21,13 +21,11 @@ package com.wire.android.ui.home.conversations.banner import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.banner.usecase.ObserveConversationMembersByTypesUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.QualifiedID @@ -38,17 +36,14 @@ import com.wire.kalium.logic.data.user.type.isFederated import com.wire.kalium.logic.data.user.type.isGuest import com.wire.kalium.logic.feature.conversation.NotifyConversationIsOpenUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch -import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel -class ConversationBannerViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class ConversationBannerViewModel( + private val conversationNavArgs: ConversationNavArgs, private val observeConversationMembersByTypes: ObserveConversationMembersByTypesUseCase, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val notifyConversationIsOpen: NotifyConversationIsOpenUseCase, @@ -56,7 +51,6 @@ class ConversationBannerViewModel @Inject constructor( var bannerState by mutableStateOf(null) - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId init { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModelFactory.kt new file mode 100644 index 00000000000..15580b86a6b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModelFactory.kt @@ -0,0 +1,38 @@ +/* + * 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.conversations.banner + +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.home.conversations.banner.usecase.ObserveConversationMembersByTypesUseCase +import com.wire.kalium.logic.feature.conversation.NotifyConversationIsOpenUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ConversationBannerViewModelFactory( + private val observeConversationMembersByTypes: ObserveConversationMembersByTypesUseCase, + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val notifyConversationIsOpen: NotifyConversationIsOpenUseCase, +) { + fun create(args: ConversationNavArgs): ConversationBannerViewModel = ConversationBannerViewModel( + conversationNavArgs = args, + observeConversationMembersByTypes = observeConversationMembersByTypes, + observeConversationDetails = observeConversationDetails, + notifyConversationIsOpen = notifyConversationIsOpen, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/usecase/ObserveConversationMembersByTypesUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/usecase/ObserveConversationMembersByTypesUseCase.kt index 9d0f7d179bf..04258ae988d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/usecase/ObserveConversationMembersByTypesUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/usecase/ObserveConversationMembersByTypesUseCase.kt @@ -26,7 +26,7 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseC import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject +import dev.zacsweers.metro.Inject class ObserveConversationMembersByTypesUseCase @Inject constructor( private val observeConversationMembers: ObserveConversationMembersUseCase, 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 8a0f0464308..ff585ab072b 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 @@ -22,13 +22,10 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.wire.android.di.CurrentAccount import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId @@ -49,7 +46,6 @@ import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNot import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import com.wire.kalium.logic.sync.ObserveSyncStateUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest @@ -57,13 +53,11 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel @Suppress("LongParameterList", "TooManyFunctions") -class ConversationCallViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, - @CurrentAccount val currentAccount: UserId, +class ConversationCallViewModel( + private val conversationNavArgs: ConversationNavArgs, + val currentAccount: UserId, private val observeOngoingCalls: ObserveOngoingCallsUseCase, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val observeParticipantsForConversation: ObserveParticipantsForConversationUseCase, @@ -78,7 +72,6 @@ class ConversationCallViewModel @Inject constructor( private val observeSelf: ObserveSelfUserUseCase ) : ActionsViewModel() { - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId var conversationCallViewState by mutableStateOf(ConversationCallViewState()) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelFactory.kt new file mode 100644 index 00000000000..027354497c3 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelFactory.kt @@ -0,0 +1,70 @@ +/* + * 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.conversations.call + +import com.wire.android.di.CurrentAccount +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase +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.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase +import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import com.wire.kalium.logic.sync.ObserveSyncStateUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class ConversationCallViewModelFactory( + @CurrentAccount private val currentAccount: UserId, + private val observeOngoingCalls: ObserveOngoingCallsUseCase, + private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, + private val observeParticipantsForConversation: ObserveParticipantsForConversationUseCase, + private val answerCall: AnswerCallUseCase, + private val endCall: EndCallUseCase, + private val observeSyncState: ObserveSyncStateUseCase, + private val isConferenceCallingEnabled: IsEligibleToStartCallUseCase, + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val setUserInformedAboutVerification: SetUserInformedAboutVerificationUseCase, + private val observeDegradedConversationNotified: ObserveDegradedConversationNotifiedUseCase, + private val observeConferenceCallingEnabled: ObserveConferenceCallingEnabledUseCase, + private val observeSelf: ObserveSelfUserUseCase, +) { + fun create(args: ConversationNavArgs): ConversationCallViewModel = ConversationCallViewModel( + conversationNavArgs = args, + currentAccount = currentAccount, + observeOngoingCalls = observeOngoingCalls, + observeEstablishedCalls = observeEstablishedCalls, + observeParticipantsForConversation = observeParticipantsForConversation, + answerCall = answerCall, + endCall = endCall, + observeSyncState = observeSyncState, + isConferenceCallingEnabled = isConferenceCallingEnabled, + observeConversationDetails = observeConversationDetails, + setUserInformedAboutVerification = setUserInformedAboutVerification, + observeDegradedConversationNotified = observeDegradedConversationNotified, + observeConferenceCallingEnabled = observeConferenceCallingEnabled, + observeSelf = observeSelf, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt index db9ade80fb7..b5ea54bb82b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt @@ -18,11 +18,9 @@ package com.wire.android.ui.home.conversations.composer -import android.net.Uri import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.datastore.GlobalDataStore @@ -32,12 +30,9 @@ import com.wire.android.ui.home.conversations.InvalidLinkDialogState import com.wire.android.ui.home.conversations.MessageComposerViewState import com.wire.android.ui.home.conversations.VisitLinkDialogState import com.wire.android.ui.home.conversations.model.UIMessage -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.EMPTY -import com.wire.android.util.FileManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.configuration.FileSharingStatus -import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode import com.wire.kalium.logic.data.conversation.InteractionAvailability import com.wire.kalium.logic.data.id.QualifiedID @@ -55,7 +50,6 @@ import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletion import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged @@ -66,12 +60,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import kotlinx.datetime.Instant -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class MessageComposerViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class MessageComposerViewModel( + conversationNavArgs: ConversationNavArgs, private val dispatchers: DispatcherProvider, private val isFileSharingEnabled: IsFileSharingEnabledUseCase, private val observeConversationInteractionAvailability: ObserveConversationInteractionAvailabilityUseCase, @@ -82,8 +74,7 @@ class MessageComposerViewModel @Inject constructor( private val enqueueMessageSelfDeletion: EnqueueMessageSelfDeletionUseCase, private val persistNewSelfDeletingStatus: PersistNewSelfDeletionTimerUseCase, private val sendTypingEvent: SendTypingEventUseCase, - private val fileManager: FileManager, - private val kaliumFileSystem: KaliumFileSystem, + private val tempWritableAttachmentUriProvider: TempWritableAttachmentUriProvider, private val currentSessionFlowUseCase: CurrentSessionFlowUseCase, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val globalDataStore: GlobalDataStore, @@ -92,13 +83,12 @@ class MessageComposerViewModel @Inject constructor( var messageComposerViewState = mutableStateOf(MessageComposerViewState()) private set - var tempWritableVideoUri: Uri? = null + var tempWritableVideoUri: String? = null private set - var tempWritableImageUri: Uri? = null + var tempWritableImageUri: String? = null private set - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId var visitLinkDialogState: VisitLinkDialogState by mutableStateOf( @@ -130,15 +120,13 @@ class MessageComposerViewModel @Inject constructor( private fun initTempWritableVideoUri() { viewModelScope.launch { - tempWritableVideoUri = - fileManager.getTempWritableVideoUri(kaliumFileSystem.rootCachePath) + tempWritableVideoUri = tempWritableAttachmentUriProvider.getTempWritableVideoUri() } } private fun initTempWritableImageUri() { viewModelScope.launch { - tempWritableImageUri = - fileManager.getTempWritableImageUri(kaliumFileSystem.rootCachePath) + tempWritableImageUri = tempWritableAttachmentUriProvider.getTempWritableImageUri() } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelFactory.kt new file mode 100644 index 00000000000..c309378a803 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelFactory.kt @@ -0,0 +1,71 @@ +/* + * 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.conversations.composer + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.mapper.ContactMapper +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase +import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase +import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase +import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase +import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase +import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class MessageComposerViewModelFactory( + private val dispatchers: DispatcherProvider, + private val isFileSharingEnabled: IsFileSharingEnabledUseCase, + private val observeConversationInteractionAvailability: ObserveConversationInteractionAvailabilityUseCase, + private val updateConversationReadDate: UpdateConversationReadDateUseCase, + private val markConversationAsReadLocally: MarkConversationAsReadLocallyUseCase, + private val contactMapper: ContactMapper, + private val membersToMention: MembersToMentionUseCase, + private val enqueueMessageSelfDeletion: EnqueueMessageSelfDeletionUseCase, + private val persistNewSelfDeletingStatus: PersistNewSelfDeletionTimerUseCase, + private val sendTypingEvent: SendTypingEventUseCase, + private val tempWritableAttachmentUriProvider: TempWritableAttachmentUriProvider, + private val currentSessionFlowUseCase: CurrentSessionFlowUseCase, + private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, + private val globalDataStore: GlobalDataStore, +) { + fun create(conversationNavArgs: ConversationNavArgs): MessageComposerViewModel = MessageComposerViewModel( + conversationNavArgs = conversationNavArgs, + dispatchers = dispatchers, + isFileSharingEnabled = isFileSharingEnabled, + observeConversationInteractionAvailability = observeConversationInteractionAvailability, + updateConversationReadDate = updateConversationReadDate, + markConversationAsReadLocally = markConversationAsReadLocally, + contactMapper = contactMapper, + membersToMention = membersToMention, + enqueueMessageSelfDeletion = enqueueMessageSelfDeletion, + persistNewSelfDeletingStatus = persistNewSelfDeletingStatus, + sendTypingEvent = sendTypingEvent, + tempWritableAttachmentUriProvider = tempWritableAttachmentUriProvider, + currentSessionFlowUseCase = currentSessionFlowUseCase, + observeEstablishedCalls = observeEstablishedCalls, + globalDataStore = globalDataStore, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/TempWritableAttachmentUriProvider.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/TempWritableAttachmentUriProvider.kt new file mode 100644 index 00000000000..4ec521e7579 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/TempWritableAttachmentUriProvider.kt @@ -0,0 +1,38 @@ +/* + * 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.conversations.composer + +import com.wire.android.util.FileManager +import com.wire.kalium.logic.data.asset.KaliumFileSystem + +interface TempWritableAttachmentUriProvider { + suspend fun getTempWritableVideoUri(): String + suspend fun getTempWritableImageUri(): String +} + +class AndroidTempWritableAttachmentUriProvider( + private val fileManager: FileManager, + private val kaliumFileSystem: KaliumFileSystem, +) : TempWritableAttachmentUriProvider { + override suspend fun getTempWritableVideoUri(): String = + fileManager.getTempWritableVideoUri(kaliumFileSystem.rootCachePath).toString() + + override suspend fun getTempWritableImageUri(): String = + fileManager.getTempWritableImageUri(kaliumFileSystem.rootCachePath).toString() +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt index 53ec0b6b3b3..fcbfb09319a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt @@ -60,10 +60,10 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.result.ResultRecipient +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.R import com.wire.android.appLogger @@ -147,7 +147,10 @@ fun GroupConversationDetailsScreen( editChannelAccessResultRecipient: ResultRecipient, conversationFoldersScreenResultRecipient: ResultRecipient, - viewModel: GroupConversationDetailsViewModel = hiltViewModel(), + args: GroupConversationDetailsNavArgs, + viewModel: GroupConversationDetailsViewModel = metroViewModel { + groupConversationDetailsViewModelFactory.create(args) + }, ) { val scope = rememberCoroutineScope() val resources = LocalContext.current.resources @@ -311,6 +314,7 @@ fun GroupConversationDetailsScreen( ) ) }, + onReadReceiptSwitchClicked = viewModel::onReadReceiptUpdate, isScreenLoading = viewModel.isFetchingInitialData ) @@ -381,6 +385,7 @@ private fun GroupConversationDetailsContent( onDeletedConversation: () -> Unit = {}, onPromoteAdmin: (ConversationId) -> Unit = {}, openConversationDebugMenu: (ConversationId) -> Unit = {}, + onReadReceiptSwitchClicked: (Boolean) -> Unit = {}, initialPageIndex: GroupConversationDetailsTabItem = GroupConversationDetailsTabItem.OPTIONS, isScreenLoading: StateFlow = MutableStateFlow(false), ) { @@ -533,11 +538,13 @@ private fun GroupConversationDetailsContent( ) { pageIndex -> when (GroupConversationDetailsTabItem.entries[pageIndex]) { GroupConversationDetailsTabItem.OPTIONS -> GroupConversationOptions( + state = groupConversationOptionsState, lazyListState = lazyListStates[pageIndex], onEditGuestAccess = onEditGuestAccess, onAppsAccessItemClicked = onAppsAccessItemClicked, onChannelAccessItemClicked = onChannelAccessItemClicked, onEditSelfDeletingMessages = onEditSelfDeletingMessages, + onReadReceiptSwitchClicked = onReadReceiptSwitchClicked, onEditGroupName = onEditGroupName ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt index dcfcdcc42b0..5cf29cbb0a3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home.conversations.details -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.ui.common.ActionsManager @@ -29,7 +28,6 @@ import com.wire.android.ui.home.conversations.details.participants.usecase.Obser import com.wire.android.ui.home.newconversation.channelaccess.ChannelAccessType import com.wire.android.ui.home.newconversation.channelaccess.ChannelAddPermissionType import com.wire.android.ui.home.newconversation.channelaccess.toUiEnum -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.AppsUtil import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.UIText @@ -48,9 +46,8 @@ import com.wire.kalium.logic.feature.featureConfig.AppsAllowedResult import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase -import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase import com.wire.kalium.logic.feature.user.IsMLSEnabledUseCase -import dagger.hilt.android.lifecycle.HiltViewModel +import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -63,11 +60,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel -class GroupConversationDetailsViewModel @Inject constructor( +class GroupConversationDetailsViewModel( + private val groupConversationDetailsNavArgs: GroupConversationDetailsNavArgs, private val dispatcher: DispatcherProvider, private val observeConversationDetails: ObserveConversationDetailsUseCase, observeConversationMembers: ObserveParticipantsForConversationUseCase, @@ -75,14 +71,16 @@ class GroupConversationDetailsViewModel @Inject constructor( private val updateConversationReceiptMode: UpdateConversationReceiptModeUseCase, private val observeSelfDeletionTimerSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase, private val observeIsAppsAllowedForUsage: ObserveIsAppsAllowedForUsageUseCase, - savedStateHandle: SavedStateHandle, private val isMLSEnabled: IsMLSEnabledUseCase, refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, private val isWireCellsEnabled: IsWireCellsEnabledUseCase, -) : GroupConversationParticipantsViewModel(savedStateHandle, observeConversationMembers, refreshUsersWithoutMetadata), +) : GroupConversationParticipantsViewModel( + groupConversationDetailsNavArgs.conversationId, + observeConversationMembers, + refreshUsersWithoutMetadata +), ActionsManager by ActionsManagerImpl() { - private val groupConversationDetailsNavArgs: GroupConversationDetailsNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = groupConversationDetailsNavArgs.conversationId private val _groupOptionsState = MutableStateFlow(GroupConversationOptionsState(conversationId)) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelFactory.kt new file mode 100644 index 00000000000..02e02d42173 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelFactory.kt @@ -0,0 +1,59 @@ +/* + * 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.conversations.details + +import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationReceiptModeUseCase +import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase +import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase +import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase +import com.wire.kalium.logic.feature.user.IsMLSEnabledUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class GroupConversationDetailsViewModelFactory( + private val dispatcher: DispatcherProvider, + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val observeConversationMembers: ObserveParticipantsForConversationUseCase, + private val observeSelfUserWithTeam: ObserveSelfUserWithTeamUseCase, + private val updateConversationReceiptMode: UpdateConversationReceiptModeUseCase, + private val observeSelfDeletionTimerSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase, + private val observeIsAppsAllowedForUsage: ObserveIsAppsAllowedForUsageUseCase, + private val isMLSEnabled: IsMLSEnabledUseCase, + private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, + private val isWireCellsEnabled: IsWireCellsEnabledUseCase, +) { + fun create(args: GroupConversationDetailsNavArgs): GroupConversationDetailsViewModel = GroupConversationDetailsViewModel( + groupConversationDetailsNavArgs = args, + dispatcher = dispatcher, + observeConversationDetails = observeConversationDetails, + observeConversationMembers = observeConversationMembers, + observeSelfUserWithTeam = observeSelfUserWithTeam, + updateConversationReceiptMode = updateConversationReceiptMode, + observeSelfDeletionTimerSettingsForConversation = observeSelfDeletionTimerSettingsForConversation, + observeIsAppsAllowedForUsage = observeIsAppsAllowedForUsage, + isMLSEnabled = isMLSEnabled, + refreshUsersWithoutMetadata = refreshUsersWithoutMetadata, + isWireCellsEnabled = isWireCellsEnabled, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt index cef330241b6..95a6e6119e3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessScreen.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.R import com.wire.android.navigation.NavigationCommand @@ -60,6 +60,7 @@ import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.copyLinkToClipboard import com.wire.android.util.shareViaIntent +import com.wire.kalium.logic.data.id.ConversationId @Suppress("ComplexMethod") @WireRootDestination( @@ -69,8 +70,10 @@ import com.wire.android.util.shareViaIntent @Composable fun EditGuestAccessScreen( navigator: Navigator, + args: EditGuestAccessNavArgs, modifier: Modifier = Modifier, - editGuestAccessViewModel: EditGuestAccessViewModel = hiltViewModel() + editGuestAccessViewModel: EditGuestAccessViewModel = + metroViewModel { editGuestAccessViewModelFactory.create(args) } ) { val scrollState = rememberScrollState() val snackbarHostState = LocalSnackbarHostState.current @@ -238,5 +241,15 @@ fun EditGuestAccessScreen( @Preview @Composable fun PreviewEditGuestAccessScreen() { - EditGuestAccessScreen(rememberNavigator {}) + EditGuestAccessScreen( + navigator = rememberNavigator {}, + args = EditGuestAccessNavArgs( + conversationId = ConversationId("conversation", "domain"), + editGuessAccessParams = EditGuestAccessParams( + isGuestAccessAllowed = true, + isServicesAllowed = true, + isUpdatingGuestAccessAllowed = true + ) + ) + ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModel.kt index 7ffeae16293..a77253a0926 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModel.kt @@ -21,13 +21,11 @@ package com.wire.android.ui.home.conversations.details.editguestaccess import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.BuildConfig import com.wire.android.appLogger import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails @@ -47,7 +45,6 @@ import com.wire.kalium.logic.feature.conversation.guestroomlink.RevokeGuestRoomL import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -59,11 +56,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel @Suppress("LongParameterList", "TooManyFunctions") -class EditGuestAccessViewModel @Inject constructor( +class EditGuestAccessViewModel( + private val editGuestAccessNavArgs: EditGuestAccessNavArgs, private val dispatcher: DispatcherProvider, private val updateConversationAccessRole: UpdateConversationAccessRoleUseCase, private val observeConversationDetails: ObserveConversationDetailsUseCase, @@ -76,10 +72,8 @@ class EditGuestAccessViewModel @Inject constructor( private val syncConversationCode: SyncConversationCodeUseCase, private val getDefaultProtocol: GetDefaultProtocolUseCase, private val selfUser: ObserveSelfUserUseCase, - savedStateHandle: SavedStateHandle ) : ViewModel() { - private val editGuestAccessNavArgs: EditGuestAccessNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = editGuestAccessNavArgs.conversationId private val accessParams = editGuestAccessNavArgs.editGuessAccessParams diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelFactory.kt new file mode 100644 index 00000000000..3d4c28d93e9 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelFactory.kt @@ -0,0 +1,65 @@ +/* + * 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.conversations.details.editguestaccess + +import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.SyncConversationCodeUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.CanCreatePasswordProtectedLinksUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.ObserveGuestRoomLinkUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.RevokeGuestRoomLinkUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class EditGuestAccessViewModelFactory( + private val dispatcher: DispatcherProvider, + private val updateConversationAccessRole: UpdateConversationAccessRoleUseCase, + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val observeConversationMembers: ObserveParticipantsForConversationUseCase, + private val generateGuestRoomLink: GenerateGuestRoomLinkUseCase, + private val revokeGuestRoomLink: RevokeGuestRoomLinkUseCase, + private val observeGuestRoomLink: ObserveGuestRoomLinkUseCase, + private val observeGuestRoomLinkFeatureFlag: ObserveGuestRoomLinkFeatureFlagUseCase, + private val canCreatePasswordProtectedLinks: CanCreatePasswordProtectedLinksUseCase, + private val syncConversationCode: SyncConversationCodeUseCase, + private val getDefaultProtocol: GetDefaultProtocolUseCase, + private val selfUser: ObserveSelfUserUseCase, +) { + fun create(args: EditGuestAccessNavArgs): EditGuestAccessViewModel = EditGuestAccessViewModel( + editGuestAccessNavArgs = args, + dispatcher = dispatcher, + updateConversationAccessRole = updateConversationAccessRole, + observeConversationDetails = observeConversationDetails, + observeConversationMembers = observeConversationMembers, + generateGuestRoomLink = generateGuestRoomLink, + revokeGuestRoomLink = revokeGuestRoomLink, + observeGuestRoomLink = observeGuestRoomLink, + observeGuestRoomLinkFeatureFlag = observeGuestRoomLinkFeatureFlag, + canCreatePasswordProtectedLinks = canCreatePasswordProtectedLinks, + syncConversationCode = syncConversationCode, + getDefaultProtocol = getDefaultProtocol, + selfUser = selfUser, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModel.kt index 25060a1db1d..a28663b55cb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModel.kt @@ -22,33 +22,27 @@ import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.ui.common.textfield.textAsFlow -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkResult import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase import com.wire.kalium.logic.util.RandomPassword -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CreatePasswordGuestLinkViewModel @Inject constructor( +class CreatePasswordGuestLinkViewModel( + createPasswordGuestLinkNavArgs: CreatePasswordGuestLinkNavArgs, private val generateGuestRoomLink: GenerateGuestRoomLinkUseCase, private val validatePassword: ValidatePasswordUseCase, private val generatePassword: RandomPassword, - savedStateHandle: SavedStateHandle ) : ViewModel() { - private val editGuestAccessNavArgs: CreatePasswordGuestLinkNavArgs = savedStateHandle.navArgs() - private val conversationId: QualifiedID = editGuestAccessNavArgs.conversationId + private val conversationId: QualifiedID = createPasswordGuestLinkNavArgs.conversationId var state by mutableStateOf(CreatePasswordGuestLinkState()) @VisibleForTesting set diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModelFactory.kt new file mode 100644 index 00000000000..d392f355910 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModelFactory.kt @@ -0,0 +1,37 @@ +/* + * 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.conversations.details.editguestaccess.createPasswordProtectedGuestLink + +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase +import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase +import com.wire.kalium.logic.util.RandomPassword +import dev.zacsweers.metro.Inject + +@Inject +class CreatePasswordGuestLinkViewModelFactory( + private val generateGuestRoomLink: GenerateGuestRoomLinkUseCase, + private val validatePassword: ValidatePasswordUseCase, + private val generatePassword: RandomPassword, +) { + fun create(args: CreatePasswordGuestLinkNavArgs): CreatePasswordGuestLinkViewModel = CreatePasswordGuestLinkViewModel( + createPasswordGuestLinkNavArgs = args, + generateGuestRoomLink = generateGuestRoomLink, + validatePassword = validatePassword, + generatePassword = generatePassword, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordProtectedGuestLinkScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordProtectedGuestLinkScreen.kt index c983328f13f..c6196ccc102 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordProtectedGuestLinkScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordProtectedGuestLinkScreen.kt @@ -46,8 +46,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.ui.common.button.GeneratePasswordButton import com.wire.android.ui.common.button.WireButtonState @@ -73,7 +73,9 @@ import kotlinx.coroutines.launch @Composable fun CreatePasswordProtectedGuestLinkScreen( navigator: Navigator, - viewModel: CreatePasswordGuestLinkViewModel = hiltViewModel(), + args: CreatePasswordGuestLinkNavArgs, + viewModel: CreatePasswordGuestLinkViewModel = + metroViewModel { createPasswordGuestLinkViewModelFactory.create(args) }, ) { CreatePasswordProtectedGuestLinkScreenContent( state = viewModel.state, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesScreen.kt index 279e79ef1d6..6b239dcdd85 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesScreen.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.R import com.wire.android.navigation.Navigator @@ -59,6 +59,7 @@ import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.ui.sectionWithElements +import com.wire.kalium.logic.data.id.ConversationId @WireRootDestination( navArgs = EditSelfDeletingMessagesNavArgs::class, @@ -67,7 +68,9 @@ import com.wire.android.util.ui.sectionWithElements @Composable fun EditSelfDeletingMessagesScreen( navigator: Navigator, - editSelfDeletingMessagesViewModel: EditSelfDeletingMessagesViewModel = hiltViewModel(), + args: EditSelfDeletingMessagesNavArgs, + editSelfDeletingMessagesViewModel: EditSelfDeletingMessagesViewModel = + metroViewModel { editSelfDeletingMessagesViewModelFactory.create(args) }, ) { val scrollState = rememberScrollState() val context = LocalContext.current @@ -169,7 +172,12 @@ fun SelectableSelfDeletingItem( @Preview @Composable fun PreviewEditSelfDeletingMessagesScreen() { - EditSelfDeletingMessagesScreen(rememberNavigator {}) + EditSelfDeletingMessagesScreen( + navigator = rememberNavigator {}, + args = EditSelfDeletingMessagesNavArgs( + conversationId = ConversationId("conversation", "domain") + ) + ) } @Preview diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModel.kt index f87c0ca6e86..ddcde109068 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModel.kt @@ -21,15 +21,13 @@ package com.wire.android.ui.home.conversations.details.editselfdeletingmessages import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger +import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration -import com.ramcosta.composedestinations.generated.app.navArgs -import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.type.isTeamAdmin @@ -37,27 +35,23 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseC import com.wire.kalium.logic.feature.conversation.messagetimer.UpdateMessageTimerUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel @Suppress("LongParameterList", "TooManyFunctions") -class EditSelfDeletingMessagesViewModel @Inject constructor( +class EditSelfDeletingMessagesViewModel( + private val editSelfDeletingMessagesNavArgs: EditSelfDeletingMessagesNavArgs, private val dispatcher: DispatcherProvider, private val observeConversationMembers: ObserveParticipantsForConversationUseCase, private val observeSelfDeletionTimerSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase, private val updateMessageTimer: UpdateMessageTimerUseCase, private val selfUser: ObserveSelfUserUseCase, private val conversationDetails: ObserveConversationDetailsUseCase, - savedStateHandle: SavedStateHandle ) : ViewModel() { - private val editSelfDeletingMessagesNavArgs: EditSelfDeletingMessagesNavArgs = savedStateHandle.navArgs() private val conversationId: QualifiedID = editSelfDeletingMessagesNavArgs.conversationId var state by mutableStateOf( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModelFactory.kt new file mode 100644 index 00000000000..ce70d91e45f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModelFactory.kt @@ -0,0 +1,46 @@ +/* + * 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.conversations.details.editselfdeletingmessages + +import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.messagetimer.UpdateMessageTimerUseCase +import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +class EditSelfDeletingMessagesViewModelFactory( + private val dispatcher: DispatcherProvider, + private val observeConversationMembers: ObserveParticipantsForConversationUseCase, + private val observeSelfDeletionTimerSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase, + private val updateMessageTimer: UpdateMessageTimerUseCase, + private val selfUser: ObserveSelfUserUseCase, + private val conversationDetails: ObserveConversationDetailsUseCase, +) { + fun create(args: EditSelfDeletingMessagesNavArgs): EditSelfDeletingMessagesViewModel = EditSelfDeletingMessagesViewModel( + editSelfDeletingMessagesNavArgs = args, + dispatcher = dispatcher, + observeConversationMembers = observeConversationMembers, + observeSelfDeletionTimerSettingsForConversation = observeSelfDeletionTimerSettingsForConversation, + updateMessageTimer = updateMessageTimer, + selfUser = selfUser, + conversationDetails = conversationDetails, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationMetadataViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationMetadataViewModel.kt index 1dc28c8e710..3dc56b327b6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationMetadataViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationMetadataViewModel.kt @@ -23,10 +23,8 @@ import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.ui.common.groupname.GroupMetadataState import com.wire.android.ui.common.groupname.GroupNameMode import com.wire.android.ui.common.groupname.GroupNameValidator @@ -37,7 +35,6 @@ import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.RenameConversationUseCase import com.wire.kalium.logic.feature.conversation.RenamingResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.flow.filterIsInstance @@ -45,17 +42,14 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel -class EditConversationMetadataViewModel @Inject constructor( +class EditConversationMetadataViewModel( + private val editConversationNameNavArgs: EditConversationNameNavArgs, private val dispatcher: DispatcherProvider, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val renameConversation: RenameConversationUseCase, - val savedStateHandle: SavedStateHandle ) : ViewModel() { - private val editConversationNameNavArgs: EditConversationNameNavArgs = savedStateHandle.navArgs() private val conversationId: QualifiedID = editConversationNameNavArgs.conversationId val editConversationNameTextState: TextFieldState = TextFieldState() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationMetadataViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationMetadataViewModelFactory.kt new file mode 100644 index 00000000000..b91b77fe7e4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationMetadataViewModelFactory.kt @@ -0,0 +1,37 @@ +/* + * 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.conversations.details.metadata + +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.RenameConversationUseCase +import dev.zacsweers.metro.Inject + +@Inject +class EditConversationMetadataViewModelFactory( + private val dispatcher: DispatcherProvider, + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val renameConversation: RenameConversationUseCase, +) { + fun create(args: EditConversationNameNavArgs): EditConversationMetadataViewModel = EditConversationMetadataViewModel( + editConversationNameNavArgs = args, + dispatcher = dispatcher, + observeConversationDetails = observeConversationDetails, + renameConversation = renameConversation, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationNameScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationNameScreen.kt index 385bb9454ea..2a13440a234 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationNameScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationNameScreen.kt @@ -22,8 +22,8 @@ import com.wire.android.navigation.annotation.app.WireRootDestination import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.navigation.Navigator import com.wire.android.ui.common.groupname.GroupMetadataState @@ -40,7 +40,9 @@ import com.wire.android.util.ui.PreviewMultipleThemes fun EditConversationNameScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, - viewModel: EditConversationMetadataViewModel = hiltViewModel(), + args: EditConversationNameNavArgs, + viewModel: EditConversationMetadataViewModel = + metroViewModel { editConversationMetadataViewModelFactory.create(args) }, ) { with(viewModel) { LaunchedEffect(editConversationState.completed) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/options/GroupConversationOptions.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/options/GroupConversationOptions.kt index 174fbf5bdbf..d7ef30d6cca 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/options/GroupConversationOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/options/GroupConversationOptions.kt @@ -32,13 +32,10 @@ import androidx.compose.material3.DividerDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.times -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.model.Clickable @@ -48,7 +45,6 @@ import com.wire.android.ui.common.WireDialogButtonType import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.rowitem.SectionHeader -import com.wire.android.ui.home.conversations.details.GroupConversationDetailsViewModel import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration import com.wire.android.ui.home.newconversation.channelaccess.ChannelAccessType import com.wire.android.ui.home.settings.SwitchState @@ -66,23 +62,22 @@ import kotlin.time.Duration.Companion.days @Composable fun GroupConversationOptions( + state: GroupConversationOptionsState, lazyListState: LazyListState, onEditGuestAccess: () -> Unit, onAppsAccessItemClicked: () -> Unit, onChannelAccessItemClicked: () -> Unit, onEditSelfDeletingMessages: () -> Unit, - viewModel: GroupConversationDetailsViewModel = hiltViewModel(), + onReadReceiptSwitchClicked: (Boolean) -> Unit, onEditGroupName: () -> Unit ) { - val state by viewModel.groupOptionsState.collectAsStateWithLifecycle() - GroupConversationSettings( state = state, onGuestItemClicked = onEditGuestAccess, onAppsAccessItemClicked = onAppsAccessItemClicked, onSelfDeletingClicked = onEditSelfDeletingMessages, onChannelAccessItemClicked = onChannelAccessItemClicked, - onReadReceiptSwitchClicked = viewModel::onReadReceiptUpdate, + onReadReceiptSwitchClicked = onReadReceiptSwitchClicked, lazyListState = lazyListState, onEditGroupName = onEditGroupName, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationAllParticipantsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationAllParticipantsScreen.kt index 02d1a31972e..2a293a005b7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationAllParticipantsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationAllParticipantsScreen.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.common.rememberTopBarElevationState @@ -57,7 +57,8 @@ import com.wire.android.ui.userprofile.service.ServiceDetailsNavArgs fun GroupConversationAllParticipantsScreen( navigator: Navigator, navArgs: GroupConversationAllParticipantsNavArgs, - viewModel: GroupConversationParticipantsViewModel = hiltViewModel() + viewModel: GroupConversationParticipantsViewModel = + metroViewModel { groupConversationParticipantsViewModelFactory.create(navArgs.conversationId) } ) { GroupConversationAllParticipantsContent( onBackPressed = navigator::navigateBack, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsViewModel.kt index 566e51a0114..50e5edaad19 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsViewModel.kt @@ -21,20 +21,15 @@ package com.wire.android.ui.home.conversations.details.participants import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -open class GroupConversationParticipantsViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +open class GroupConversationParticipantsViewModel( + private val conversationId: QualifiedID, private val observeConversationMembers: ObserveParticipantsForConversationUseCase, private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, ) : ViewModel() { @@ -43,9 +38,6 @@ open class GroupConversationParticipantsViewModel @Inject constructor( var groupParticipantsState: GroupConversationParticipantsState by mutableStateOf(GroupConversationParticipantsState()) - private val groupConversationAllParticipantsNavArgs: GroupConversationAllParticipantsNavArgs = savedStateHandle.navArgs() - private val conversationId: QualifiedID = groupConversationAllParticipantsNavArgs.conversationId - init { runRefreshUsersWithoutMetadata() observeConversationMembers() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsViewModelFactory.kt new file mode 100644 index 00000000000..7705cd4bfd9 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsViewModelFactory.kt @@ -0,0 +1,35 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.details.participants + +import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase +import dev.zacsweers.metro.Inject + +@Inject +class GroupConversationParticipantsViewModelFactory( + private val observeConversationMembers: ObserveParticipantsForConversationUseCase, + private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, +) { + fun create(conversationId: QualifiedID): GroupConversationParticipantsViewModel = GroupConversationParticipantsViewModel( + conversationId = conversationId, + observeConversationMembers = observeConversationMembers, + refreshUsersWithoutMetadata = refreshUsersWithoutMetadata, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessScreen.kt index 5debdd21736..540fac1476b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessScreen.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.navigation.annotation.app.WireRootDestination import com.wire.android.navigation.style.SlideNavigationAnimation @@ -54,7 +54,9 @@ import kotlinx.coroutines.launch @Composable fun UpdateAppsAccessScreen( navigator: Navigator, - updateAppsAccessViewModel: UpdateAppsAccessViewModel = hiltViewModel() + args: UpdateAppsAccessNavArgs, + updateAppsAccessViewModel: UpdateAppsAccessViewModel = + metroViewModel { updateAppsAccessViewModelFactory.create(args) } ) { UpdateAppsAccessContent( onNavigateBack = navigator::navigateBack, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModel.kt index 18d9c15c0c5..dc9e2328d9e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModel.kt @@ -20,11 +20,9 @@ package com.wire.android.ui.home.conversations.details.updateappsaccess import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.AppsUtil import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.conversation.Conversation @@ -39,7 +37,6 @@ import com.wire.kalium.logic.feature.conversation.apps.ChangeAccessForAppsInConv import com.wire.kalium.logic.feature.featureConfig.AppsAllowedResult import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -49,20 +46,17 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel -class UpdateAppsAccessViewModel @Inject constructor( +class UpdateAppsAccessViewModel( + private val updateAppsAccessNavArgs: UpdateAppsAccessNavArgs, private val dispatcher: DispatcherProvider, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val observeConversationMembers: ObserveParticipantsForConversationUseCase, private val observeIsAppsAllowedForUsage: ObserveIsAppsAllowedForUsageUseCase, private val selfUser: ObserveSelfUserUseCase, private val changeAccessForAppsInConversation: ChangeAccessForAppsInConversationUseCase, - savedStateHandle: SavedStateHandle ) : ViewModel() { - private val updateAppsAccessNavArgs: UpdateAppsAccessNavArgs = savedStateHandle.navArgs() private val conversationId: QualifiedID = updateAppsAccessNavArgs.conversationId private val currentAccessParams = updateAppsAccessNavArgs.updateAppsAccessParams val shouldUseNewAppsUi: Boolean = currentAccessParams.shouldUseNewAppsUi diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModelFactory.kt new file mode 100644 index 00000000000..0dd4b64006c --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModelFactory.kt @@ -0,0 +1,46 @@ +/* + * 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.conversations.details.updateappsaccess + +import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.apps.ChangeAccessForAppsInConversationUseCase +import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +class UpdateAppsAccessViewModelFactory( + private val dispatcher: DispatcherProvider, + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val observeConversationMembers: ObserveParticipantsForConversationUseCase, + private val observeIsAppsAllowedForUsage: ObserveIsAppsAllowedForUsageUseCase, + private val selfUser: ObserveSelfUserUseCase, + private val changeAccessForAppsInConversation: ChangeAccessForAppsInConversationUseCase, +) { + fun create(args: UpdateAppsAccessNavArgs): UpdateAppsAccessViewModel = UpdateAppsAccessViewModel( + updateAppsAccessNavArgs = args, + dispatcher = dispatcher, + observeConversationDetails = observeConversationDetails, + observeConversationMembers = observeConversationMembers, + observeIsAppsAllowedForUsage = observeIsAppsAllowedForUsage, + selfUser = selfUser, + changeAccessForAppsInConversation = changeAccessForAppsInConversation, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/ChannelAccessOnUpdateScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/ChannelAccessOnUpdateScreen.kt index af35f41469e..a0ab28aeea6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/ChannelAccessOnUpdateScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/ChannelAccessOnUpdateScreen.kt @@ -21,10 +21,10 @@ import com.wire.android.navigation.annotation.app.WireRootDestination import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.NavigationIconType @@ -38,7 +38,9 @@ import com.wire.android.ui.home.newconversation.channelaccess.ChannelAccessScree @Composable fun ChannelAccessOnUpdateScreen( resultNavigator: ResultBackNavigator, - updateChannelAccessViewModel: UpdateChannelAccessViewModel = hiltViewModel() + args: UpdateChannelAccessArgs, + updateChannelAccessViewModel: UpdateChannelAccessViewModel = + metroViewModel { updateChannelAccessViewModelFactory.create(args) } ) { fun navigateBack() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModel.kt index facef7e0686..e0e5d3a4373 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModel.kt @@ -20,30 +20,23 @@ package com.wire.android.ui.home.conversations.details.updatechannelaccess import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.ui.home.newconversation.channelaccess.ChannelAccessType import com.wire.android.ui.home.newconversation.channelaccess.ChannelAddPermissionType import com.wire.android.ui.home.newconversation.channelaccess.toDomainEnum -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.toQualifiedID import com.wire.kalium.logic.feature.conversation.channel.UpdateChannelAddPermissionUseCase import com.wire.kalium.logic.feature.conversation.channel.UpdateChannelAddPermissionUseCase.UpdateChannelAddPermissionUseCaseResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class UpdateChannelAccessViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class UpdateChannelAccessViewModel( + private val channelAccessNavArgs: UpdateChannelAccessArgs, val updateChannelAddPermission: UpdateChannelAddPermissionUseCase, private val qualifiedIdMapper: QualifiedIdMapper, ) : ViewModel() { - private val channelAccessNavArgs: UpdateChannelAccessArgs = savedStateHandle.navArgs() - var accessType: ChannelAccessType by mutableStateOf(channelAccessNavArgs.accessType) private set diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModelFactory.kt new file mode 100644 index 00000000000..b3163e24e01 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModelFactory.kt @@ -0,0 +1,34 @@ +/* + * 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.conversations.details.updatechannelaccess + +import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.feature.conversation.channel.UpdateChannelAddPermissionUseCase +import dev.zacsweers.metro.Inject + +@Inject +class UpdateChannelAccessViewModelFactory( + private val updateChannelAddPermission: UpdateChannelAddPermissionUseCase, + private val qualifiedIdMapper: QualifiedIdMapper, +) { + fun create(args: UpdateChannelAccessArgs): UpdateChannelAccessViewModel = UpdateChannelAccessViewModel( + channelAccessNavArgs = args, + updateChannelAddPermission = updateChannelAddPermission, + qualifiedIdMapper = qualifiedIdMapper, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuViewModel.kt index 7f44758e9c9..1df67513cb1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuViewModel.kt @@ -19,17 +19,12 @@ package com.wire.android.ui.home.conversations.edit import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.di.AssistedViewModelFactory import com.wire.android.di.ScopedArgs import com.wire.android.di.ViewModelScopedPreview import com.wire.android.ui.home.conversations.mock.mockMessageWithText import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.usecase.ObserveMessageForConversationUseCase import com.wire.kalium.logic.data.id.QualifiedID -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -48,10 +43,9 @@ interface MessageOptionsMenuViewModel { MutableStateFlow(MessageOptionsMenuState.Message(mockMessageWithText)) } -@HiltViewModel(assistedFactory = MessageOptionsMenuViewModelImpl.Factory::class) -class MessageOptionsMenuViewModelImpl @AssistedInject constructor( +class MessageOptionsMenuViewModelImpl( private val observeMessageForConversation: ObserveMessageForConversationUseCase, - @Assisted private val args: MessageOptionsMenuArgs, + private val args: MessageOptionsMenuArgs, ) : MessageOptionsMenuViewModel, ViewModel() { private val messageStateFlow: ConcurrentHashMap> = ConcurrentHashMap() @@ -75,11 +69,6 @@ class MessageOptionsMenuViewModelImpl @AssistedInject constructor( initialValue = MessageOptionsMenuState.Loading, ) } - - @AssistedFactory - interface Factory : AssistedViewModelFactory { - override fun create(args: MessageOptionsMenuArgs): MessageOptionsMenuViewModelImpl - } } @Serializable diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuViewModelFactory.kt new file mode 100644 index 00000000000..e0843c50e95 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuViewModelFactory.kt @@ -0,0 +1,31 @@ +/* + * 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.conversations.edit + +import com.wire.android.ui.home.conversations.usecase.ObserveMessageForConversationUseCase +import dev.zacsweers.metro.Inject + +@Inject +class MessageOptionsMenuViewModelFactory( + private val observeMessageForConversation: ObserveMessageForConversationUseCase, +) { + fun create(args: MessageOptionsMenuArgs): MessageOptionsMenuViewModelImpl = MessageOptionsMenuViewModelImpl( + observeMessageForConversation = observeMessageForConversation, + args = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsModalSheetLayout.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsModalSheetLayout.kt index 05f0ef49ce2..ff73076fc8a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsModalSheetLayout.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsModalSheetLayout.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader import com.wire.android.ui.common.bottomsheet.WireMenuModalSheetContent import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout @@ -62,11 +62,11 @@ fun MessageOptionsModalSheetLayout( onDownloadAssetClick: (messageId: String) -> Unit, onOpenAssetClick: (messageId: String) -> Unit, viewModel: MessageOptionsMenuViewModel = - hiltViewModelScoped< + wireViewModelScoped< MessageOptionsMenuViewModelImpl, MessageOptionsMenuViewModel, MessageOptionsMenuArgs, - MessageOptionsMenuViewModelImpl.Factory + MessageOptionsMenuViewModelFactory, >(MessageOptionsMenuArgs(conversationId)) ) { val context = LocalContext.current diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersScreen.kt index 53a18c31b53..4d38e7e1705 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersScreen.kt @@ -35,11 +35,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.result.ResultRecipient import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.Clickable import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -71,15 +71,13 @@ fun ConversationFoldersScreen( resultNavigator: ResultBackNavigator, resultRecipient: ResultRecipient, foldersViewModel: ConversationFoldersVM = - hiltViewModel( - creationCallback = { it.create(ConversationFoldersStateArgs(args.currentFolderId)) } - ), + metroViewModel { conversationFoldersViewModelFactory.create(ConversationFoldersStateArgs(args.currentFolderId)) }, moveToFolderVM: MoveConversationToFolderVM = - hiltViewModel( - creationCallback = { - it.create(MoveConversationToFolderArgs(args.conversationId, args.conversationName, args.currentFolderId)) - } - ) + metroViewModel { + moveConversationToFolderViewModelFactory.create( + MoveConversationToFolderArgs(args.conversationId, args.conversationName, args.currentFolderId) + ) + } ) { val resources = LocalContext.current.resources diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersVM.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersVM.kt index 1973d92ed05..fe0f0e58a9b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersVM.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersVM.kt @@ -25,10 +25,6 @@ import androidx.lifecycle.viewModelScope import com.wire.android.di.ViewModelScopedPreview import com.wire.kalium.logic.data.conversation.ConversationFolder import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -41,17 +37,11 @@ interface ConversationFoldersVM { fun onFolderSelected(folderId: String) {} } -@HiltViewModel(assistedFactory = ConversationFoldersVMImpl.Factory::class) -class ConversationFoldersVMImpl @AssistedInject constructor( - @Assisted val args: ConversationFoldersStateArgs, +class ConversationFoldersVMImpl( + val args: ConversationFoldersStateArgs, private val observeUserFoldersUseCase: ObserveUserFoldersUseCase, ) : ConversationFoldersVM, ViewModel() { - @AssistedFactory - interface Factory { - fun create(args: ConversationFoldersStateArgs): ConversationFoldersVMImpl - } - private var state by mutableStateOf(ConversationFoldersState(persistentListOf(), args.selectedFolderId)) override fun state(): ConversationFoldersState = state diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersViewModelFactory.kt new file mode 100644 index 00000000000..2cc5fd2c37b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersViewModelFactory.kt @@ -0,0 +1,31 @@ +/* + * 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.conversations.folder + +import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ConversationFoldersViewModelFactory( + private val observeUserFoldersUseCase: ObserveUserFoldersUseCase, +) { + fun create(args: ConversationFoldersStateArgs): ConversationFoldersVMImpl = ConversationFoldersVMImpl( + args = args, + observeUserFoldersUseCase = observeUserFoldersUseCase, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderVM.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderVM.kt index d1112cc5e11..e45c8c86dd7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderVM.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderVM.kt @@ -29,10 +29,6 @@ import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.conversation.ConversationFolder import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.conversation.folder.MoveConversationToFolderUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -49,20 +45,14 @@ interface MoveConversationToFolderVM { fun moveConversationToFolder(folder: ConversationFolder) {} } -@HiltViewModel(assistedFactory = MoveConversationToFolderVMImpl.Factory::class) -class MoveConversationToFolderVMImpl @AssistedInject constructor( +class MoveConversationToFolderVMImpl( private val dispatchers: DispatcherProvider, - @Assisted val args: MoveConversationToFolderArgs, + val args: MoveConversationToFolderArgs, private val moveConversationToFolder: MoveConversationToFolderUseCase, ) : MoveConversationToFolderVM, ViewModel() { private var state: MoveConversationToFolderState by mutableStateOf(MoveConversationToFolderState()) - @AssistedFactory - interface Factory { - fun create(args: MoveConversationToFolderArgs): MoveConversationToFolderVMImpl - } - private val _infoMessage = MutableSharedFlow() override val infoMessage = _infoMessage.asSharedFlow() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderViewModelFactory.kt new file mode 100644 index 00000000000..acaae2755eb --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderViewModelFactory.kt @@ -0,0 +1,34 @@ +/* + * 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.conversations.folder + +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.conversation.folder.MoveConversationToFolderUseCase +import dev.zacsweers.metro.Inject + +@Inject +class MoveConversationToFolderViewModelFactory( + private val dispatchers: DispatcherProvider, + private val moveConversationToFolder: MoveConversationToFolderUseCase, +) { + fun create(args: MoveConversationToFolderArgs): MoveConversationToFolderVMImpl = MoveConversationToFolderVMImpl( + dispatchers = dispatchers, + args = args, + moveConversationToFolder = moveConversationToFolder, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewConversationFolderScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewConversationFolderScreen.kt index 2153976ee83..edb08b28e4b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewConversationFolderScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewConversationFolderScreen.kt @@ -36,9 +36,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.ui.common.animation.ShakeAnimation @@ -68,7 +68,9 @@ import com.wire.android.util.ui.SnackBarMessageHandler fun NewConversationFolderScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, - viewModel: NewFolderViewModel = hiltViewModel() + viewModel: NewFolderViewModel = metroViewModel { + newFolderViewModelFactory.create() + } ) { LaunchedEffect(viewModel.folderNameState.folderId) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewFolderViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewFolderViewModel.kt index 5489d3c8b65..cb716e10bdc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewFolderViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewFolderViewModel.kt @@ -30,16 +30,13 @@ import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.util.ui.UIText import com.wire.kalium.logic.feature.conversation.folder.CreateConversationFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class NewFolderViewModel @Inject constructor( +class NewFolderViewModel( private val observeUserFolders: ObserveUserFoldersUseCase, private val createConversationFolder: CreateConversationFolderUseCase ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewFolderViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewFolderViewModelFactory.kt new file mode 100644 index 00000000000..d1942d43686 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewFolderViewModelFactory.kt @@ -0,0 +1,33 @@ +/* + * 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.conversations.folder + +import com.wire.kalium.logic.feature.conversation.folder.CreateConversationFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase +import dev.zacsweers.metro.Inject + +@Inject +class NewFolderViewModelFactory( + private val observeUserFolders: ObserveUserFoldersUseCase, + private val createConversationFolder: CreateConversationFolderUseCase, +) { + fun create(): NewFolderViewModel = NewFolderViewModel( + observeUserFolders = observeUserFolders, + createConversationFolder = createConversationFolder, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt index 43f49220cdd..511ab852a28 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt @@ -21,15 +21,12 @@ package com.wire.android.ui.home.conversations.info import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.appLogger -import com.wire.android.di.CurrentAccount import com.wire.android.model.ImageAsset import com.wire.android.ui.home.conversations.ConversationNavArgs -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.ui.UIText import com.wire.android.util.ui.toUIText import com.wire.kalium.common.error.StorageFailure @@ -41,22 +38,17 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.e2ei.usecase.FetchConversationMLSVerificationStatusUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class ConversationInfoViewModel @Inject constructor( +class ConversationInfoViewModel( + private val conversationNavArgs: ConversationNavArgs, private val qualifiedIdMapper: QualifiedIdMapper, - val savedStateHandle: SavedStateHandle, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val fetchConversationMLSVerificationStatus: FetchConversationMLSVerificationStatusUseCase, private val isWireCellFeatureEnabled: IsWireCellsEnabledUseCase, - @CurrentAccount private val selfUserId: UserId, + private val selfUserId: UserId, ) : ViewModel() { - - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId var conversationInfoViewState by mutableStateOf(ConversationInfoViewState(conversationId)) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelFactory.kt new file mode 100644 index 00000000000..a97684534c1 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelFactory.kt @@ -0,0 +1,45 @@ +/* + * 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.conversations.info + +import com.wire.android.di.CurrentAccount +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.FetchConversationMLSVerificationStatusUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ConversationInfoViewModelFactory( + private val qualifiedIdMapper: QualifiedIdMapper, + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val fetchConversationMLSVerificationStatus: FetchConversationMLSVerificationStatusUseCase, + private val isWireCellFeatureEnabled: IsWireCellsEnabledUseCase, + @CurrentAccount private val selfUserId: UserId, +) { + fun create(args: ConversationNavArgs): ConversationInfoViewModel = ConversationInfoViewModel( + conversationNavArgs = args, + qualifiedIdMapper = qualifiedIdMapper, + observeConversationDetails = observeConversationDetails, + fetchConversationMLSVerificationStatus = fetchConversationMLSVerificationStatus, + isWireCellFeatureEnabled = isWireCellFeatureEnabled, + selfUserId = selfUserId, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt index de231ec65c3..af485c945ae 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModel.kt @@ -24,11 +24,8 @@ import androidx.lifecycle.ViewModel import com.wire.android.ui.home.conversations.AssetTooLargeDialogState import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.sharing.ImportedMediaAsset -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -@HiltViewModel -class CheckAssetRestrictionsViewModel @Inject constructor() : ViewModel() { +class CheckAssetRestrictionsViewModel : ViewModel() { var state: RestrictionCheckState by mutableStateOf(RestrictionCheckState.None) private set diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModelFactory.kt new file mode 100644 index 00000000000..9fa8bc6e9b0 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModelFactory.kt @@ -0,0 +1,25 @@ +/* + * 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.conversations.media + +import dev.zacsweers.metro.Inject + +@Inject +class CheckAssetRestrictionsViewModelFactory { + fun create(): CheckAssetRestrictionsViewModel = CheckAssetRestrictionsViewModel() +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModel.kt index 7115fee6ece..978ad1cf490 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModel.kt @@ -21,31 +21,24 @@ package com.wire.android.ui.home.conversations.media import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.usecase.GetAssetMessagesFromConversationUseCase import com.wire.android.ui.home.conversations.usecase.ObserveImageAssetMessagesFromConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.feature.asset.ObserveAssetStatusesUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel @Suppress("LongParameterList", "TooManyFunctions") -class ConversationAssetMessagesViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class ConversationAssetMessagesViewModel( + conversationMediaNavArgs: ConversationMediaNavArgs, private val getImageMessages: ObserveImageAssetMessagesFromConversationUseCase, private val getAssetMessages: GetAssetMessagesFromConversationUseCase, private val observeAssetStatuses: ObserveAssetStatusesUseCase, ) : ViewModel() { - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() - val conversationId: QualifiedID = conversationNavArgs.conversationId + val conversationId: QualifiedID = conversationMediaNavArgs.conversationId var viewState by mutableStateOf(ConversationAssetMessagesViewState()) private set diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModelFactory.kt new file mode 100644 index 00000000000..4bf5ce3d1e2 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModelFactory.kt @@ -0,0 +1,37 @@ +/* + * 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.conversations.media + +import com.wire.android.ui.home.conversations.usecase.GetAssetMessagesFromConversationUseCase +import com.wire.android.ui.home.conversations.usecase.ObserveImageAssetMessagesFromConversationUseCase +import com.wire.kalium.logic.feature.asset.ObserveAssetStatusesUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ConversationAssetMessagesViewModelFactory( + private val getImageMessages: ObserveImageAssetMessagesFromConversationUseCase, + private val getAssetMessages: GetAssetMessagesFromConversationUseCase, + private val observeAssetStatuses: ObserveAssetStatusesUseCase, +) { + fun create(args: ConversationMediaNavArgs): ConversationAssetMessagesViewModel = ConversationAssetMessagesViewModel( + conversationMediaNavArgs = args, + getImageMessages = getImageMessages, + getAssetMessages = getAssetMessages, + observeAssetStatuses = observeAssetStatuses, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt index 972009eefbb..bd9dff1b550 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationMediaScreen.kt @@ -42,8 +42,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.PopUpNavigationAnimation @@ -63,6 +63,7 @@ import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState import com.ramcosta.composedestinations.generated.app.destinations.MediaGalleryScreenDestination +import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.ConversationSnackbarMessages import com.wire.android.ui.home.conversations.DownloadedAssetDialog import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState @@ -87,8 +88,13 @@ import kotlinx.serialization.Serializable @Composable fun ConversationMediaScreen( navigator: Navigator, - conversationAssetMessagesViewModel: ConversationAssetMessagesViewModel = hiltViewModel(), - conversationMessagesViewModel: ConversationMessagesViewModel = hiltViewModel() + args: ConversationMediaNavArgs, + conversationAssetMessagesViewModel: ConversationAssetMessagesViewModel = metroViewModel { + conversationAssetMessagesViewModelFactory.create(args) + }, + conversationMessagesViewModel: ConversationMessagesViewModel = metroViewModel { + conversationMessagesViewModelFactory.create(ConversationNavArgs(args.conversationId)) + } ) { val permissionPermanentlyDeniedDialogState = rememberVisibilityState() val context = LocalContext.current @@ -125,7 +131,7 @@ fun ConversationMediaScreen( conversationMessagesViewModel.deleteMessageDialogState .show(DeleteMessageDialogState(deleteForEveryone, messageId, conversationMessagesViewModel.conversationId)) }, - shareAsset = remember { { conversationMessagesViewModel.shareAsset(context, it) } }, + shareAsset = remember { conversationMessagesViewModel::shareAsset }, downloadAsset = conversationMessagesViewModel::openOrFetchAsset, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewAssetImporter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewAssetImporter.kt new file mode 100644 index 00000000000..c8c6b249194 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewAssetImporter.kt @@ -0,0 +1,45 @@ +/* + * 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.conversations.media.preview + +import androidx.core.net.toUri +import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase +import com.wire.android.ui.sharing.ImportedMediaAsset +import com.wire.android.util.dispatchers.DispatcherProvider +import kotlinx.coroutines.withContext + +interface ImagesPreviewAssetImporter { + suspend fun importAsset(uri: String): ImportedMediaAsset? +} + +class ImagesPreviewAssetImporterImpl( + private val handleUriAsset: HandleUriAssetUseCase, + private val dispatchers: DispatcherProvider, +) : ImagesPreviewAssetImporter { + + override suspend fun importAsset(uri: String): ImportedMediaAsset? = withContext(dispatchers.io()) { + when (val result = handleUriAsset.invoke(uri.toUri(), saveToDeviceIfInvalid = false)) { + is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> ImportedMediaAsset( + result.assetBundle, + result.maxLimitInMB + ) + HandleUriAssetUseCase.Result.Failure.Unknown -> null + is HandleUriAssetUseCase.Result.Success -> ImportedMediaAsset(result.assetBundle, null) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewModule.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewModule.kt new file mode 100644 index 00000000000..32488a66755 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewModule.kt @@ -0,0 +1,20 @@ +/* + * 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.conversations.media.preview + +// Images preview bindings are provided by the Metro graph. diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt index 623c901f51b..a9700071b5f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewScreen.kt @@ -49,9 +49,9 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.ui.common.button.WirePrimaryButton @@ -89,8 +89,13 @@ import okio.Path.Companion.toPath fun ImagesPreviewScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, - imagesPreviewViewModel: ImagesPreviewViewModel = hiltViewModel(), - checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = hiltViewModel() + args: ImagesPreviewNavArgs, + imagesPreviewViewModel: ImagesPreviewViewModel = metroViewModel { + imagesPreviewViewModelFactory.create(args) + }, + checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = metroViewModel { + checkAssetRestrictionsViewModelFactory.create() + } ) { LaunchedEffect(checkAssetRestrictionsViewModel.state) { with(checkAssetRestrictionsViewModel.state) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModel.kt index a377bf8dd91..6dc5b003190 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModel.kt @@ -17,31 +17,19 @@ */ package com.wire.android.ui.home.conversations.media.preview -import android.net.Uri import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase -import com.ramcosta.composedestinations.generated.app.navArgs -import com.wire.android.ui.sharing.ImportedMediaAsset -import com.wire.android.util.dispatchers.DispatcherProvider -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel -class ImagesPreviewViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, - private val handleUriAsset: HandleUriAssetUseCase, - private val dispatchers: DispatcherProvider +class ImagesPreviewViewModel( + private val navArgs: ImagesPreviewNavArgs, + private val assetImporter: ImagesPreviewAssetImporter ) : ViewModel() { - private val navArgs: ImagesPreviewNavArgs = savedStateHandle.navArgs() var viewState by mutableStateOf( ImagesPreviewState( conversationId = navArgs.conversationId, @@ -65,20 +53,11 @@ class ImagesPreviewViewModel @Inject constructor( private fun handleAssets() { viewState = viewState.copy(isLoading = true) viewModelScope.launch { - val assets = navArgs.assetUriList.map { handleImportedAsset(it) } + val assets = navArgs.assetUriList.map { assetImporter.importAsset(it.toString()) } viewState = viewState.copy( assetBundleList = assets.filterNotNull().toPersistentList(), isLoading = false ) } } - - private suspend fun handleImportedAsset(uri: Uri): ImportedMediaAsset? = withContext(dispatchers.io()) { - when (val result = handleUriAsset.invoke(uri, saveToDeviceIfInvalid = false)) { - is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> ImportedMediaAsset(result.assetBundle, result.maxLimitInMB) - - HandleUriAssetUseCase.Result.Failure.Unknown -> null - is HandleUriAssetUseCase.Result.Success -> ImportedMediaAsset(result.assetBundle, null) - } - } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModelFactory.kt new file mode 100644 index 00000000000..45ffc5f1b68 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.conversations.media.preview + +import dev.zacsweers.metro.Inject + +@Inject +class ImagesPreviewViewModelFactory( + private val assetImporter: ImagesPreviewAssetImporter, +) { + fun create(args: ImagesPreviewNavArgs): ImagesPreviewViewModel = ImagesPreviewViewModel( + navArgs = args, + assetImporter = assetImporter, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsScreen.kt index 3b74b3604f5..9510708d1be 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsScreen.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home.conversations.messagedetails -import com.wire.android.navigation.annotation.app.WireRootDestination import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.LocalOverscrollConfiguration @@ -44,9 +43,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator +import com.wire.android.navigation.annotation.app.WireRootDestination import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.ui.common.TabItem import com.wire.android.ui.common.WireTabRow @@ -67,7 +67,8 @@ import kotlinx.coroutines.launch @Composable fun MessageDetailsScreen( navigator: Navigator, - viewModel: MessageDetailsViewModel = hiltViewModel() + args: MessageDetailsNavArgs, + viewModel: MessageDetailsViewModel = metroViewModel { messageDetailsViewModelFactory.create(args) } ) { val context = LocalContext.current diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsViewModel.kt index 55e4c1eabd7..14221a0a3ed 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsViewModel.kt @@ -21,26 +21,20 @@ package com.wire.android.ui.home.conversations.messagedetails import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveReactionsForMessageUseCase import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveReceiptsForMessageUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.message.receipt.ReceiptType -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class MessageDetailsViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class MessageDetailsViewModel( + private val messageDetailsNavArgs: MessageDetailsNavArgs, private val observeReactionsForMessage: ObserveReactionsForMessageUseCase, private val observeReceiptsForMessage: ObserveReceiptsForMessageUseCase ) : ViewModel() { - private val messageDetailsNavArgs: MessageDetailsNavArgs = savedStateHandle.navArgs() private val conversationId: QualifiedID = messageDetailsNavArgs.conversationId private val messageId: String = messageDetailsNavArgs.messageId private val isSelfMessage: Boolean = messageDetailsNavArgs.isSelfMessage diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsViewModelFactory.kt new file mode 100644 index 00000000000..f3f38f5cc8f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsViewModelFactory.kt @@ -0,0 +1,34 @@ +/* + * 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.conversations.messagedetails + +import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveReactionsForMessageUseCase +import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveReceiptsForMessageUseCase +import dev.zacsweers.metro.Inject + +@Inject +class MessageDetailsViewModelFactory( + private val observeReactionsForMessage: ObserveReactionsForMessageUseCase, + private val observeReceiptsForMessage: ObserveReceiptsForMessageUseCase, +) { + fun create(args: MessageDetailsNavArgs): MessageDetailsViewModel = MessageDetailsViewModel( + messageDetailsNavArgs = args, + observeReactionsForMessage = observeReactionsForMessage, + observeReceiptsForMessage = observeReceiptsForMessage, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/usecase/ObserveReactionsForMessageUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/usecase/ObserveReactionsForMessageUseCase.kt index 7c366bff401..cb003c33ed5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/usecase/ObserveReactionsForMessageUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/usecase/ObserveReactionsForMessageUseCase.kt @@ -27,7 +27,7 @@ import com.wire.kalium.logic.feature.message.ObserveMessageReactionsUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject +import dev.zacsweers.metro.Inject class ObserveReactionsForMessageUseCase @Inject constructor( private val observeMessageReactions: ObserveMessageReactionsUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/usecase/ObserveReceiptsForMessageUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/usecase/ObserveReceiptsForMessageUseCase.kt index b0465bbc21b..d4e16770f47 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/usecase/ObserveReceiptsForMessageUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/usecase/ObserveReceiptsForMessageUseCase.kt @@ -27,7 +27,7 @@ import com.wire.kalium.logic.feature.message.ObserveMessageReceiptsUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject +import dev.zacsweers.metro.Inject class ObserveReceiptsForMessageUseCase @Inject constructor( private val observeMessageReceipts: ObserveMessageReceiptsUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationAssetFileGateway.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationAssetFileGateway.kt new file mode 100644 index 00000000000..86b0d665b1a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationAssetFileGateway.kt @@ -0,0 +1,49 @@ +/* + * 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.conversations.messages + +import com.wire.android.util.FileManager +import okio.Path + +interface ConversationAssetFileGateway { + fun openWithExternalApp(assetDataPath: Path, assetName: String?, onError: () -> Unit) + suspend fun saveToExternalStorage(assetName: String, assetDataPath: Path, assetSize: Long): String? + fun shareWithExternalApp(assetDataPath: Path, assetName: String?) +} + +class AndroidConversationAssetFileGateway( + private val fileManager: FileManager, +) : ConversationAssetFileGateway { + + override fun openWithExternalApp(assetDataPath: Path, assetName: String?, onError: () -> Unit) { + fileManager.openWithExternalApp(assetDataPath, assetName, onError) + } + + override suspend fun saveToExternalStorage(assetName: String, assetDataPath: Path, assetSize: Long): String? { + var savedFileName: String? = null + fileManager.saveToExternalStorage(assetName, assetDataPath, assetSize) { + savedFileName = it + } + return savedFileName + } + + override fun shareWithExternalApp(assetDataPath: Path, assetName: String?) { + fileManager.shareWithExternalApp(assetDataPath, assetName) {} + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt index 1de13e8ec59..49f11d0deec 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt @@ -18,14 +18,11 @@ package com.wire.android.ui.home.conversations.messages -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.R import com.wire.android.appLogger import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer @@ -40,9 +37,7 @@ import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.android.ui.home.conversations.model.UIQuotedMessage import com.wire.android.ui.home.conversations.usecase.GetMessagesForConversationUseCase -import com.wire.android.util.FileManager import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.startFileShareIntent import com.wire.android.util.ui.UIText import com.wire.kalium.common.functional.onFailure import com.wire.kalium.logic.data.asset.AssetTransferStatus @@ -68,7 +63,6 @@ import com.wire.kalium.logic.feature.message.GetSearchedConversationMessagePosit import com.wire.kalium.logic.feature.message.ToggleReactionUseCase import com.wire.kalium.logic.feature.sessionreset.ResetSessionResult import com.wire.kalium.logic.feature.sessionreset.ResetSessionUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -84,20 +78,18 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okio.Path -import javax.inject.Inject import kotlin.math.max import kotlin.time.Duration.Companion.seconds -@HiltViewModel @Suppress("LongParameterList", "TooManyFunctions") -class ConversationMessagesViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class ConversationMessagesViewModel( + private val conversationNavArgs: ConversationNavArgs, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val getMessageAsset: GetMessageAssetUseCase, private val getMessageByIdUseCase: GetMessageByIdUseCase, private val updateAssetMessageDownloadStatus: UpdateAssetMessageTransferStatusUseCase, private val observeAssetStatusesUseCase: ObserveAssetStatusesUseCase, - private val fileManager: FileManager, + private val assetFileGateway: ConversationAssetFileGateway, private val dispatchers: DispatcherProvider, private val getMessageForConversation: GetMessagesForConversationUseCase, private val fetchOlderNomadMessages: FetchOlderNomadMessagesByConversationUseCase, @@ -111,7 +103,6 @@ class ConversationMessagesViewModel @Inject constructor( private val isWireCellFeatureEnabled: IsWireCellsEnabledUseCase, ) : ViewModel() { - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId private val searchedMessageIdNavArgs: String? = conversationNavArgs.searchedMessageId @@ -340,7 +331,7 @@ class ConversationMessagesViewModel @Inject constructor( private fun onOpenFileWithExternalApp(assetDataPath: Path, assetName: String?) { viewModelScope.launch { withContext(dispatchers.io()) { - fileManager.openWithExternalApp(assetDataPath, assetName) { onOpenFileError() } + assetFileGateway.openWithExternalApp(assetDataPath, assetName) { onOpenFileError() } hideOnAssetDownloadedDialog() } } @@ -349,11 +340,10 @@ class ConversationMessagesViewModel @Inject constructor( private fun onSaveFile(assetName: String, assetDataPath: Path, assetSize: Long, messageId: String) { viewModelScope.launch { withContext(dispatchers.io()) { - fileManager.saveToExternalStorage(assetName, assetDataPath, assetSize) { savedFileName: String? -> - updateAssetMessageDownloadStatus(AssetTransferStatus.SAVED_EXTERNALLY, conversationId, messageId) - onFileSavedToExternalStorage(savedFileName) - hideOnAssetDownloadedDialog() - } + val savedFileName = assetFileGateway.saveToExternalStorage(assetName, assetDataPath, assetSize) + updateAssetMessageDownloadStatus(AssetTransferStatus.SAVED_EXTERNALLY, conversationId, messageId) + onFileSavedToExternalStorage(savedFileName) + hideOnAssetDownloadedDialog() } } } @@ -394,10 +384,10 @@ class ConversationMessagesViewModel @Inject constructor( } } - fun shareAsset(context: Context, messageId: String) { + fun shareAsset(messageId: String) { viewModelScope.launch { assetDataPath(conversationId, messageId)?.run { - context.startFileShareIntent(first, second) + assetFileGateway.shareWithExternalApp(first, second) } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelFactory.kt new file mode 100644 index 00000000000..d0c8f625736 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelFactory.kt @@ -0,0 +1,80 @@ +/* + * 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.conversations.messages + +import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.home.conversations.usecase.GetMessagesForConversationUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase +import com.wire.kalium.logic.feature.asset.ObserveAssetStatusesUseCase +import com.wire.kalium.logic.feature.asset.UpdateAssetMessageTransferStatusUseCase +import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase +import com.wire.kalium.logic.feature.conversation.ClearUsersTypingEventsUseCase +import com.wire.kalium.logic.feature.conversation.GetConversationUnreadEventsCountUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.message.DeleteMessageUseCase +import com.wire.kalium.logic.feature.message.FetchOlderNomadMessagesByConversationUseCase +import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase +import com.wire.kalium.logic.feature.message.GetSearchedConversationMessagePositionUseCase +import com.wire.kalium.logic.feature.message.ToggleReactionUseCase +import com.wire.kalium.logic.feature.sessionreset.ResetSessionUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class ConversationMessagesViewModelFactory( + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val getMessageAsset: GetMessageAssetUseCase, + private val getMessageByIdUseCase: GetMessageByIdUseCase, + private val updateAssetMessageDownloadStatus: UpdateAssetMessageTransferStatusUseCase, + private val observeAssetStatusesUseCase: ObserveAssetStatusesUseCase, + private val assetFileGateway: ConversationAssetFileGateway, + private val dispatchers: DispatcherProvider, + private val getMessageForConversation: GetMessagesForConversationUseCase, + private val fetchOlderNomadMessages: FetchOlderNomadMessagesByConversationUseCase, + private val toggleReaction: ToggleReactionUseCase, + private val resetSession: ResetSessionUseCase, + private val audioMessagePlayer: ConversationAudioMessagePlayer, + private val getConversationUnreadEventsCount: GetConversationUnreadEventsCountUseCase, + private val clearUsersTypingEvents: ClearUsersTypingEventsUseCase, + private val getSearchedConversationMessagePosition: GetSearchedConversationMessagePositionUseCase, + private val deleteMessage: DeleteMessageUseCase, + private val isWireCellFeatureEnabled: IsWireCellsEnabledUseCase, +) { + fun create(conversationNavArgs: ConversationNavArgs): ConversationMessagesViewModel = ConversationMessagesViewModel( + conversationNavArgs = conversationNavArgs, + observeConversationDetails = observeConversationDetails, + getMessageAsset = getMessageAsset, + getMessageByIdUseCase = getMessageByIdUseCase, + updateAssetMessageDownloadStatus = updateAssetMessageDownloadStatus, + observeAssetStatusesUseCase = observeAssetStatusesUseCase, + assetFileGateway = assetFileGateway, + dispatchers = dispatchers, + getMessageForConversation = getMessageForConversation, + fetchOlderNomadMessages = fetchOlderNomadMessages, + toggleReaction = toggleReaction, + resetSession = resetSession, + audioMessagePlayer = audioMessagePlayer, + getConversationUnreadEventsCount = getConversationUnreadEventsCount, + clearUsersTypingEvents = clearUsersTypingEvents, + getSearchedConversationMessagePosition = getSearchedConversationMessagePosition, + deleteMessage = deleteMessage, + isWireCellFeatureEnabled = isWireCellFeatureEnabled, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt index 273de122af4..244e6d5c204 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMessage.kt @@ -55,7 +55,7 @@ import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import coil3.compose.SubcomposeAsyncImage import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset import com.wire.android.ui.common.StatusBox @@ -65,10 +65,11 @@ import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.typography import com.wire.android.ui.home.conversations.LocalAssetLocalPathKeyInScopeResolver -import com.wire.android.ui.home.conversations.messages.item.MessageStyle import com.wire.android.ui.home.conversations.messages.item.AssetLocalPathArgs import com.wire.android.ui.home.conversations.messages.item.AssetLocalPathViewModel +import com.wire.android.ui.home.conversations.messages.item.AssetLocalPathViewModelFactory import com.wire.android.ui.home.conversations.messages.item.AssetLocalPathViewModelImpl +import com.wire.android.ui.home.conversations.messages.item.MessageStyle import com.wire.android.ui.home.conversations.messages.item.highlighted import com.wire.android.ui.home.conversations.messages.item.isBubble import com.wire.android.ui.home.conversations.messages.item.textColor @@ -622,24 +623,22 @@ private fun QuotedImageThumbnail( val keyInScopeResolver = LocalAssetLocalPathKeyInScopeResolver.current val viewModel: AssetLocalPathViewModel = if (keyInScopeResolver != null && keyInScopeResolver(args.key)) { - hiltViewModelScoped< + wireViewModelScoped< AssetLocalPathViewModelImpl, AssetLocalPathViewModel, AssetLocalPathArgs, - AssetLocalPathViewModelImpl.Factory, + AssetLocalPathViewModelFactory, >( arguments = args, keyInScopeResolver = keyInScopeResolver, ) } else { - hiltViewModelScoped< + wireViewModelScoped< AssetLocalPathViewModelImpl, AssetLocalPathViewModel, AssetLocalPathArgs, - AssetLocalPathViewModelImpl.Factory, - >( - args - ) + AssetLocalPathViewModelFactory, + >(args) } LaunchedEffect(Unit) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessage.kt index cfbddc6185c..69f7bd4c8ef 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessage.kt @@ -45,12 +45,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade import coil3.video.VideoFrameDecoder import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.cells.domain.model.AttachmentFileType import com.wire.android.feature.cells.domain.model.AttachmentFileType.VIDEO import com.wire.android.feature.cells.domain.model.icon @@ -75,7 +75,9 @@ fun QuotedMultipartMessage( accent: Accent, clickable: Clickable?, modifier: Modifier = Modifier, - viewModel: QuotedMultipartMessageViewModel = hiltViewModel(key = conversationId.toString()), + viewModel: QuotedMultipartMessageViewModel = metroViewModel(key = conversationId.toString()) { + quotedMultipartMessageViewModelFactory.create() + }, startContent: @Composable () -> Unit = {} ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModel.kt index 67412744fe5..a1a3d24bbff 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModel.kt @@ -25,15 +25,12 @@ import com.wire.android.ui.home.conversations.model.UIMultipartQuotedContent import com.wire.android.ui.home.conversations.model.UIQuotedMessage import com.wire.android.ui.home.conversations.usecase.ObserveQuoteMessageForConversationUseCase import com.wire.kalium.logic.data.id.ConversationId -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull -import javax.inject.Inject -@HiltViewModel -class QuotedMultipartMessageViewModel @Inject constructor( +class QuotedMultipartMessageViewModel( private val observeQuotedMessage: ObserveQuoteMessageForConversationUseCase, ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModelFactory.kt new file mode 100644 index 00000000000..4e5ab6d5a39 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.conversations.messages + +import com.wire.android.ui.home.conversations.usecase.ObserveQuoteMessageForConversationUseCase +import dev.zacsweers.metro.Inject + +@Inject +class QuotedMultipartMessageViewModelFactory( + private val observeQuotedMessage: ObserveQuoteMessageForConversationUseCase, +) { + fun create(): QuotedMultipartMessageViewModel = QuotedMultipartMessageViewModel( + observeQuotedMessage = observeQuotedMessage, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModel.kt index 90734d89972..75e476d8a08 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModel.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home.conversations.messages.draft import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.ui.home.conversations.ConversationNavArgs @@ -28,25 +27,20 @@ import com.wire.android.ui.home.conversations.usecase.GetQuoteMessageForConversa import com.wire.android.ui.home.messagecomposer.model.MessageComposition import com.wire.android.ui.home.messagecomposer.model.toDraft import com.wire.android.ui.home.messagecomposer.model.update -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.EMPTY import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.message.draft.MessageDraft import com.wire.kalium.logic.feature.message.draft.GetMessageDraftUseCase import com.wire.kalium.logic.feature.message.draft.SaveMessageDraftUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class MessageDraftViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class MessageDraftViewModel( + private val conversationNavArgs: ConversationNavArgs, private val getMessageDraft: GetMessageDraftUseCase, private val getQuotedMessage: GetQuoteMessageForConversationUseCase, private val saveMessageDraft: SaveMessageDraftUseCase, ) : ViewModel() { - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId var state = mutableStateOf(MessageComposition(conversationId, String.EMPTY)) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelFactory.kt new file mode 100644 index 00000000000..67f8f560566 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelFactory.kt @@ -0,0 +1,38 @@ +/* + * 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.conversations.messages.draft + +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.home.conversations.usecase.GetQuoteMessageForConversationUseCase +import com.wire.kalium.logic.feature.message.draft.GetMessageDraftUseCase +import com.wire.kalium.logic.feature.message.draft.SaveMessageDraftUseCase +import dev.zacsweers.metro.Inject + +@Inject +class MessageDraftViewModelFactory( + private val getMessageDraft: GetMessageDraftUseCase, + private val getQuotedMessage: GetQuoteMessageForConversationUseCase, + private val saveMessageDraft: SaveMessageDraftUseCase, +) { + fun create(conversationNavArgs: ConversationNavArgs): MessageDraftViewModel = MessageDraftViewModel( + conversationNavArgs = conversationNavArgs, + getMessageDraft = getMessageDraft, + getQuotedMessage = getQuotedMessage, + saveMessageDraft = saveMessageDraft, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/AssetLocalPathViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/AssetLocalPathViewModel.kt index 45c31dcc46f..ad8ddd8241f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/AssetLocalPathViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/AssetLocalPathViewModel.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.di.AssistedViewModelFactory import com.wire.android.di.ScopedArgs import com.wire.android.di.ViewModelScopedPreview import com.wire.android.util.dispatchers.DispatcherProvider @@ -31,10 +30,6 @@ import com.wire.kalium.logic.data.asset.AssetTransferStatus import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import com.wire.kalium.logic.feature.asset.MessageAssetResult -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -57,11 +52,10 @@ interface AssetLocalPathViewModel { ) {} } -@HiltViewModel(assistedFactory = AssetLocalPathViewModelImpl.Factory::class) -internal class AssetLocalPathViewModelImpl @AssistedInject constructor( +class AssetLocalPathViewModelImpl( private val getMessageAsset: GetMessageAssetUseCase, private val dispatchers: DispatcherProvider, - @Assisted private val args: AssetLocalPathArgs, + private val args: AssetLocalPathArgs, ) : ViewModel(), AssetLocalPathViewModel { override var localAssetPath: String? by mutableStateOf(null) private set @@ -99,9 +93,4 @@ internal class AssetLocalPathViewModelImpl @AssistedInject constructor( } } } - - @AssistedFactory - interface Factory : AssistedViewModelFactory { - override fun create(args: AssetLocalPathArgs): AssetLocalPathViewModelImpl - } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/AssetLocalPathViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/AssetLocalPathViewModelFactory.kt new file mode 100644 index 00000000000..6a39be0e95f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/AssetLocalPathViewModelFactory.kt @@ -0,0 +1,34 @@ +/* + * 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.conversations.messages.item + +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase +import dev.zacsweers.metro.Inject + +@Inject +class AssetLocalPathViewModelFactory( + private val getMessageAsset: GetMessageAssetUseCase, + private val dispatchers: DispatcherProvider, +) { + fun create(args: AssetLocalPathArgs): AssetLocalPathViewModelImpl = AssetLocalPathViewModelImpl( + getMessageAsset = getMessageAsset, + dispatchers = dispatchers, + args = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/ConversationAssetPathsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/ConversationAssetPathsViewModel.kt index 487ac253790..204a39d0058 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/ConversationAssetPathsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/ConversationAssetPathsViewModel.kt @@ -30,11 +30,9 @@ import com.wire.kalium.logic.data.asset.AssetTransferStatus.UPLOADED import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import com.wire.kalium.logic.feature.asset.MessageAssetResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject interface ConversationAssetPathsViewModel { fun localAssetPath(messageId: String): String? = null @@ -48,8 +46,7 @@ interface ConversationAssetPathsViewModel { object ConversationAssetPathsViewModelPreview : ConversationAssetPathsViewModel -@HiltViewModel -class ConversationAssetPathsViewModelImpl @Inject constructor( +class ConversationAssetPathsViewModelImpl( private val getMessageAsset: GetMessageAssetUseCase, private val dispatchers: DispatcherProvider, ) : ViewModel(), ConversationAssetPathsViewModel { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/ConversationAssetPathsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/ConversationAssetPathsViewModelFactory.kt new file mode 100644 index 00000000000..1e3f8c24a4d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/ConversationAssetPathsViewModelFactory.kt @@ -0,0 +1,33 @@ +/* + * 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.conversations.messages.item + +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ConversationAssetPathsViewModelFactory( + private val getMessageAsset: GetMessageAssetUseCase, + private val dispatchers: DispatcherProvider, +) { + fun create(): ConversationAssetPathsViewModelImpl = ConversationAssetPathsViewModelImpl( + getMessageAsset = getMessageAsset, + dispatchers = dispatchers, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContentAndStatus.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContentAndStatus.kt index 5ecf1daebb0..1ca84386873 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContentAndStatus.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContentAndStatus.kt @@ -25,12 +25,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.hilt.navigation.compose.hiltViewModel import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.media.audiomessage.AudioMessageArgs import com.wire.android.model.Clickable import com.wire.android.ui.common.applyIf @@ -79,7 +79,11 @@ internal fun UIMessage.Regular.MessageContentAndStatus( ) { val conversationAssetPathsViewModel: ConversationAssetPathsViewModel = when { LocalInspectionMode.current -> ConversationAssetPathsViewModelPreview - else -> hiltViewModel(key = message.conversationId.toString()) + else -> metroViewModel( + key = message.conversationId.toString() + ) { + conversationAssetPathsViewModelFactory.create() + } } val onAssetClickable = remember(message) { @@ -239,6 +243,7 @@ private fun MessageContent( VerticalSpace.x4() } MessageBody( + conversationId = message.conversationId, messageBody = messageContent.messageBody, searchQuery = searchQuery, isAvailable = !message.isPending && message.isAvailable, @@ -281,6 +286,7 @@ private fun MessageContent( VerticalSpace.x4() } MessageBody( + conversationId = message.conversationId, messageBody = messageContent.messageBody, isAvailable = !message.isPending && message.isAvailable, onOpenProfile = onOpenProfile, @@ -403,6 +409,7 @@ private fun MessageContent( } if (messageContent.messageBody?.message?.asString()?.isNotEmpty() == true) { MessageBody( + conversationId = message.conversationId, messageBody = messageContent.messageBody, searchQuery = searchQuery, isAvailable = !message.isPending && message.isAvailable, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt index 57a0a48bd50..550569f4b7a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModel.kt @@ -20,25 +20,20 @@ package com.wire.android.ui.home.conversations.migration import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.ui.home.conversations.ConversationNavArgs -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class ConversationMigrationViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class ConversationMigrationViewModel( + private val conversationNavArgs: ConversationNavArgs, private val observeConversationDetails: ObserveConversationDetailsUseCase ) : ViewModel() { @@ -51,7 +46,6 @@ class ConversationMigrationViewModel @Inject constructor( var migratedConversationId by mutableStateOf(null) private set - private val conversationNavArgs = savedStateHandle.navArgs() private val conversationId: QualifiedID = conversationNavArgs.conversationId init { @@ -67,6 +61,6 @@ class ConversationMigrationViewModel @Inject constructor( migratedConversationId = activeOneOnOneConversationId } } + } } - } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelFactory.kt new file mode 100644 index 00000000000..e01c1365bbb --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelFactory.kt @@ -0,0 +1,32 @@ +/* + * 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.conversations.migration + +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ConversationMigrationViewModelFactory( + private val observeConversationDetails: ObserveConversationDetailsUseCase, +) { + fun create(args: ConversationNavArgs): ConversationMigrationViewModel = ConversationMigrationViewModel( + conversationNavArgs = args, + observeConversationDetails = observeConversationDetails, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/CompositeMessageArgs.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/CompositeMessageArgs.kt index 43280b17575..71c45ccd252 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/CompositeMessageArgs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/CompositeMessageArgs.kt @@ -18,13 +18,15 @@ package com.wire.android.ui.home.conversations.model import com.wire.android.di.ScopedArgs +import com.wire.kalium.logic.data.id.ConversationId import kotlinx.serialization.Serializable @Serializable data class CompositeMessageArgs( + val conversationId: ConversationId, val messageId: String ) : ScopedArgs { - override val key = "$ARGS_KEY:$messageId" + override val key = "$ARGS_KEY:$conversationId:$messageId" companion object { const val ARGS_KEY = "CompositeMessageArgsKey" diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt index d53987364d2..e6db3f99ad3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/MessageTypes.kt @@ -43,7 +43,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.unit.DpSize -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset import com.wire.android.ui.common.applyIf @@ -56,6 +56,7 @@ import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.spacers.VerticalSpace import com.wire.android.ui.home.conversations.CompositeMessageViewModel +import com.wire.android.ui.home.conversations.CompositeMessageViewModelFactory import com.wire.android.ui.home.conversations.CompositeMessageViewModelImpl import com.wire.android.ui.home.conversations.messages.item.MessageStyle import com.wire.android.ui.home.conversations.messages.item.error @@ -88,6 +89,7 @@ import com.wire.kalium.logic.data.asset.AssetTransferStatus.FAILED_DOWNLOAD import com.wire.kalium.logic.data.asset.AssetTransferStatus.FAILED_UPLOAD import com.wire.kalium.logic.data.asset.AssetTransferStatus.NOT_FOUND import com.wire.kalium.logic.data.asset.AssetTransferStatus.UPLOAD_IN_PROGRESS +import com.wire.kalium.logic.data.id.ConversationId import kotlinx.collections.immutable.PersistentList import okio.Path @@ -95,6 +97,7 @@ import okio.Path // waiting for the backend to implement mapping logic for the MessageBody @Composable internal fun MessageBody( + conversationId: ConversationId, messageId: String, messageBody: MessageBody?, isAvailable: Boolean, @@ -154,6 +157,7 @@ internal fun MessageBody( buttonList?.also { VerticalSpace.x4() MessageButtonsContent( + conversationId = conversationId, messageId = messageId, buttonList = it, messageStyle = messageStyle @@ -163,19 +167,18 @@ internal fun MessageBody( @Composable fun MessageButtonsContent( + conversationId: ConversationId, messageId: String, buttonList: List, messageStyle: MessageStyle, modifier: Modifier = Modifier, viewModel: CompositeMessageViewModel = - hiltViewModelScoped< + wireViewModelScoped< CompositeMessageViewModelImpl, CompositeMessageViewModel, CompositeMessageArgs, - CompositeMessageViewModelImpl.Factory - >( - CompositeMessageArgs(messageId) - ) + CompositeMessageViewModelFactory + >(CompositeMessageArgs(conversationId, messageId)) ) { Column( modifier = modifier diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/audio/AudioMessageType.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/audio/AudioMessageType.kt index 741ba8cf0d8..ecf8bdfdf57 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/audio/AudioMessageType.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/audio/AudioMessageType.kt @@ -65,10 +65,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.media.audiomessage.AudioMediaPlayingState import com.wire.android.media.audiomessage.AudioMessageArgs import com.wire.android.media.audiomessage.AudioMessageViewModel +import com.wire.android.media.audiomessage.AudioMessageViewModelFactory import com.wire.android.media.audiomessage.AudioMessageViewModelImpl import com.wire.android.media.audiomessage.AudioSpeed import com.wire.android.media.audiomessage.AudioState @@ -182,18 +183,18 @@ private fun UploadedAudioMessage( ) { val keyInScopeResolver = LocalAudioMessageKeyInScopeResolver.current val viewModel: AudioMessageViewModel = if (keyInScopeResolver != null) { - hiltViewModelScoped< + wireViewModelScoped< AudioMessageViewModelImpl, AudioMessageViewModel, AudioMessageArgs, - AudioMessageViewModelImpl.Factory + AudioMessageViewModelFactory >(audioMessageArgs, keyInScopeResolver) } else { - hiltViewModelScoped< + wireViewModelScoped< AudioMessageViewModelImpl, AudioMessageViewModel, AudioMessageArgs, - AudioMessageViewModelImpl.Factory + AudioMessageViewModelFactory >(audioMessageArgs) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsView.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsView.kt index e306a561dff..732f83d2814 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsView.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsView.kt @@ -30,10 +30,10 @@ import androidx.compose.ui.layout.onVisibilityChanged import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode -import androidx.hilt.navigation.compose.hiltViewModel import coil3.decode.Decoder import coil3.request.ImageRequest import coil3.request.crossfade +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.multipart.MultipartAttachmentUi @@ -59,7 +59,9 @@ fun MultipartAttachmentsView( modifier: Modifier = Modifier, viewModel: MultipartAttachmentsViewModel = when { LocalInspectionMode.current -> MultipartAttachmentsViewModelPreview - else -> hiltViewModel(key = conversationId.value) + else -> metroViewModel(key = conversationId.value) { + multipartAttachmentsViewModelFactory.create() + } } ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt index abff1e8ad23..08282852e67 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModel.kt @@ -40,11 +40,9 @@ import com.wire.kalium.logic.data.message.AssetContent import com.wire.kalium.logic.data.message.CellAssetContent import com.wire.kalium.logic.data.message.MessageAttachment import com.wire.kalium.logic.featureFlags.KaliumConfigs -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import okio.Path.Companion.toPath -import javax.inject.Inject interface MultipartAttachmentsViewModel { fun onClick(attachment: MultipartAttachmentUi, openInImageViewer: (String) -> Unit) @@ -100,8 +98,7 @@ object MultipartAttachmentsViewModelPreview : MultipartAttachmentsViewModel { override fun onAttachmentsHidden(attachments: List) {} } -@HiltViewModel -class MultipartAttachmentsViewModelImpl @Inject constructor( +class MultipartAttachmentsViewModelImpl( private val refreshHelper: CellAssetRefreshHelper, private val download: DownloadCellFileUseCase, private val getEditorUrl: GetEditorUrlUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModelFactory.kt new file mode 100644 index 00000000000..6ebb0f4c47a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModelFactory.kt @@ -0,0 +1,51 @@ +/* + * 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.conversations.model.messagetypes.multipart + +import com.wire.android.feature.cells.ui.edit.OnlineEditor +import com.wire.android.util.FileManager +import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase +import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase +import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase +import com.wire.kalium.logic.data.asset.KaliumFileSystem +import com.wire.kalium.logic.featureFlags.KaliumConfigs +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class MultipartAttachmentsViewModelFactory( + private val refreshHelper: CellAssetRefreshHelper, + private val download: DownloadCellFileUseCase, + private val getEditorUrl: GetEditorUrlUseCase, + private val onlineEditor: OnlineEditor, + private val fileManager: FileManager, + private val kaliumFileSystem: KaliumFileSystem, + private val featureFlags: KaliumConfigs, + private val getWireCellsConfig: GetWireCellConfigurationUseCase, +) { + fun create(): MultipartAttachmentsViewModelImpl = MultipartAttachmentsViewModelImpl( + refreshHelper = refreshHelper, + download = download, + getEditorUrl = getEditorUrl, + onlineEditor = onlineEditor, + fileManager = fileManager, + kaliumFileSystem = kaliumFileSystem, + featureFlags = featureFlags, + getWireCellsConfig = getWireCellsConfig, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminScreen.kt index 965b10ee321..95a9a1dfd5a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminScreen.kt @@ -39,9 +39,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.Clickable import com.wire.android.model.ItemActionType import com.wire.android.navigation.Navigator @@ -74,7 +74,10 @@ import com.wire.android.ui.common.R as commonR @Composable fun PromoteAdminScreen( navigator: Navigator, - viewModel: PromoteAdminViewModel = hiltViewModel(), + navArgs: PromoteAdminNavArgs, + viewModel: PromoteAdminViewModel = metroViewModel { + promoteAdminViewModelFactory.create(navArgs) + }, ) { val state by viewModel.state.collectAsStateWithLifecycle() val snackbarHostState = LocalSnackbarHostState.current diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModel.kt index a2a55445e40..ce93981367f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModel.kt @@ -17,9 +17,7 @@ */ package com.wire.android.ui.home.conversations.promoteadmin -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.home.conversations.avatar @@ -29,7 +27,6 @@ import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConversationAdminRoleUseCase import com.wire.kalium.logic.feature.conversation.PromoteAdminAndLeaveConversationUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -37,18 +34,14 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel -class PromoteAdminViewModel @Inject constructor( +class PromoteAdminViewModel( private val promoteAdminAndLeave: PromoteAdminAndLeaveConversationUseCase, private val observeEligibleMembers: ObserveEligibleMembersForConversationAdminRoleUseCase, private val dispatchers: DispatcherProvider, - savedStateHandle: SavedStateHandle, + private val navArgs: PromoteAdminNavArgs, ) : ActionsViewModel() { - private val navArgs: PromoteAdminNavArgs = savedStateHandle.navArgs() - private val allMembers = MutableStateFlow>(emptyList()) private val searchQuery = MutableStateFlow("") private val selectedUserId = MutableStateFlow(null) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelFactory.kt new file mode 100644 index 00000000000..76db418e014 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelFactory.kt @@ -0,0 +1,37 @@ +/* + * 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.conversations.promoteadmin + +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConversationAdminRoleUseCase +import com.wire.kalium.logic.feature.conversation.PromoteAdminAndLeaveConversationUseCase +import dev.zacsweers.metro.Inject + +@Inject +class PromoteAdminViewModelFactory( + private val promoteAdminAndLeave: PromoteAdminAndLeaveConversationUseCase, + private val observeEligibleMembers: ObserveEligibleMembersForConversationAdminRoleUseCase, + private val dispatchers: DispatcherProvider, +) { + fun create(navArgs: PromoteAdminNavArgs): PromoteAdminViewModel = PromoteAdminViewModel( + promoteAdminAndLeave = promoteAdminAndLeave, + observeEligibleMembers = observeEligibleMembers, + dispatchers = dispatchers, + navArgs = navArgs, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt index 302ee9ece88..a4b8d2194c1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModel.kt @@ -20,13 +20,11 @@ package com.wire.android.ui.home.conversations.search import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.mapper.ContactMapper import com.wire.android.ui.common.DEFAULT_SEARCH_QUERY_DEBOUNCE import com.wire.android.ui.home.newconversation.model.Contact -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.EMPTY import com.wire.kalium.logic.feature.auth.ValidateUserHandleResult import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase @@ -35,7 +33,6 @@ import com.wire.kalium.logic.feature.search.IsFederationSearchAllowedUseCase import com.wire.kalium.logic.feature.search.SearchByHandleUseCase import com.wire.kalium.logic.feature.search.SearchUserResult import com.wire.kalium.logic.feature.search.SearchUsersUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf @@ -48,26 +45,17 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class SearchUserViewModel @Inject constructor( +class SearchUserViewModel( + private val addMembersSearchNavArgs: AddMembersSearchNavArgs?, private val searchUserUseCase: SearchUsersUseCase, private val searchByHandleUseCase: SearchByHandleUseCase, private val contactMapper: ContactMapper, private val federatedSearchParser: FederatedSearchParser, private val validateUserHandle: ValidateUserHandleUseCase, - private val isFederationSearchAllowed: IsFederationSearchAllowedUseCase, - savedStateHandle: SavedStateHandle + private val isFederationSearchAllowed: IsFederationSearchAllowedUseCase ) : ViewModel() { - @Suppress("TooGenericExceptionCaught") - private val addMembersSearchNavArgs: AddMembersSearchNavArgs? = try { - savedStateHandle.navArgs() - } catch (e: RuntimeException) { - null - } - private val searchQueryTextFlow = MutableStateFlow(String.EMPTY) private val selectedContactsFlow = MutableStateFlow>(persistentSetOf()) var state: SearchUserState by mutableStateOf(SearchUserState(isLoading = true)) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelFactory.kt new file mode 100644 index 00000000000..e89fdc9cd00 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelFactory.kt @@ -0,0 +1,46 @@ +/* + * 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.conversations.search + +import com.wire.android.mapper.ContactMapper +import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase +import com.wire.kalium.logic.feature.search.FederatedSearchParser +import com.wire.kalium.logic.feature.search.IsFederationSearchAllowedUseCase +import com.wire.kalium.logic.feature.search.SearchByHandleUseCase +import com.wire.kalium.logic.feature.search.SearchUsersUseCase +import dev.zacsweers.metro.Inject + +@Inject +class SearchUserViewModelFactory( + private val searchUserUseCase: SearchUsersUseCase, + private val searchByHandleUseCase: SearchByHandleUseCase, + private val contactMapper: ContactMapper, + private val federatedSearchParser: FederatedSearchParser, + private val validateUserHandle: ValidateUserHandleUseCase, + private val isFederationSearchAllowed: IsFederationSearchAllowedUseCase, +) { + fun create(addMembersSearchNavArgs: AddMembersSearchNavArgs?): SearchUserViewModel = SearchUserViewModel( + addMembersSearchNavArgs = addMembersSearchNavArgs, + searchUserUseCase = searchUserUseCase, + searchByHandleUseCase = searchByHandleUseCase, + contactMapper = contactMapper, + federatedSearchParser = federatedSearchParser, + validateUserHandle = validateUserHandle, + isFederationSearchAllowed = isFederationSearchAllowed, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt index 80878b665d9..60d660b104a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt @@ -45,8 +45,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.ItemActionType import com.wire.android.ui.common.CollapsingTopBarScaffold import com.wire.android.ui.common.TabItem @@ -86,6 +86,7 @@ fun SearchUsersAndAppsScreen( isConversationAppsEnabled: Boolean = true, initialPage: SearchPeopleTabItem = SearchPeopleTabItem.PEOPLE, conversationProtocol: Conversation.ProtocolInfo? = null, + addMembersSearchNavArgs: AddMembersSearchNavArgs? = null, onContinue: () -> Unit = {}, onCreateNewGroup: () -> Unit = {}, onCreateNewChannel: () -> Unit = {}, @@ -185,6 +186,7 @@ fun SearchUsersAndAppsScreen( isSearchActive = searchBarState.isSearchActive, actionType = actionType, lazyListState = lazyListStates[pageIndex], + addMembersSearchNavArgs = addMembersSearchNavArgs, ) } @@ -261,7 +263,12 @@ private fun SearchAllPeopleOrContactsScreen( actionType: ItemActionType, onOpenUserProfile: (Contact) -> Unit, onContactChecked: (Boolean, Contact) -> Unit, - searchUserViewModel: SearchUserViewModel = hiltViewModel(), + addMembersSearchNavArgs: AddMembersSearchNavArgs? = null, + searchUserViewModel: SearchUserViewModel = metroViewModel( + key = "search_user_${addMembersSearchNavArgs?.conversationId?.value ?: "new_conversation"}", + ) { + searchUserViewModelFactory.create(addMembersSearchNavArgs) + }, lazyListState: LazyListState = rememberLazyListState(), ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersSearchScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersSearchScreen.kt index 80f22280160..5bfc5471206 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersSearchScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersSearchScreen.kt @@ -20,8 +20,8 @@ package com.wire.android.ui.home.conversations.search.adddembertoconversation import com.wire.android.navigation.annotation.app.WireRootDestination import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination @@ -42,7 +42,9 @@ import com.wire.kalium.logic.data.user.UserId fun AddMembersSearchScreen( navigator: Navigator, navArgs: AddMembersSearchNavArgs, - addMembersToConversationViewModel: AddMembersToConversationViewModel = hiltViewModel(), + addMembersToConversationViewModel: AddMembersToConversationViewModel = metroViewModel { + addMembersToConversationViewModelFactory.create(navArgs) + }, ) { if (addMembersToConversationViewModel.newGroupState.isCompleted) { navigator.navigateBack() @@ -75,6 +77,7 @@ fun AddMembersSearchScreen( isUserAllowedToCreateChannels = false, shouldShowChannelPromotion = false, isConversationAppsEnabled = navArgs.isConversationAppsEnabled, - conversationProtocol = navArgs.protocolInfo + conversationProtocol = navArgs.protocolInfo, + addMembersSearchNavArgs = navArgs ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModel.kt index 57e19d195ee..d0e9afbd1eb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModel.kt @@ -21,32 +21,25 @@ package com.wire.android.ui.home.conversations.search.adddembertoconversation import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.ui.home.conversations.search.AddMembersSearchNavArgs import com.wire.android.ui.home.newconversation.model.Contact -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.AddMemberToConversationUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel -class AddMembersToConversationViewModel @Inject constructor( +class AddMembersToConversationViewModel( + private val addMembersSearchNavArgs: AddMembersSearchNavArgs, private val addMemberToConversation: AddMemberToConversationUseCase, - private val dispatchers: DispatcherProvider, - savedStateHandle: SavedStateHandle + private val dispatchers: DispatcherProvider ) : ViewModel() { - private val addMembersSearchNavArgs: AddMembersSearchNavArgs = savedStateHandle.navArgs() - var newGroupState: AddMembersToConversationState by mutableStateOf(AddMembersToConversationState()) private set diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModelFactory.kt new file mode 100644 index 00000000000..35d01ed80b0 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModelFactory.kt @@ -0,0 +1,35 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.adddembertoconversation + +import com.wire.android.ui.home.conversations.search.AddMembersSearchNavArgs +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.conversation.AddMemberToConversationUseCase +import dev.zacsweers.metro.Inject + +@Inject +class AddMembersToConversationViewModelFactory( + private val addMemberToConversation: AddMemberToConversationUseCase, + private val dispatchers: DispatcherProvider, +) { + fun create(args: AddMembersSearchNavArgs): AddMembersToConversationViewModel = AddMembersToConversationViewModel( + addMembersSearchNavArgs = args, + addMemberToConversation = addMemberToConversation, + dispatchers = dispatchers, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsScreen.kt index d125b3e7ef2..4da111a10c5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsScreen.kt @@ -35,8 +35,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.Clickable import com.wire.android.ui.common.ArrowRightIcon import com.wire.android.ui.common.UserBadge @@ -66,10 +66,11 @@ fun SearchAppsScreen( searchQuery: String, onServiceClicked: (Contact) -> Unit, isConversationAppsEnabled: Boolean, - searchAppsViewModel: SearchAppsViewModel = hiltViewModel( + searchAppsViewModel: SearchAppsViewModel = metroViewModel( key = "search_apps_protocol_info_${protocolInfo?.name()}", - creationCallback = { factory -> factory.create(protocolInfo = protocolInfo) } - ), + ) { + searchAppsViewModelFactory.create(protocolInfo = protocolInfo) + }, lazyListState: LazyListState = rememberLazyListState() ) { LaunchedEffect(key1 = searchQuery) { 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 793e4d438aa..6192faa2d46 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 @@ -37,10 +37,6 @@ 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 -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -52,9 +48,8 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = SearchAppsViewModel.Factory::class) -class SearchAppsViewModel @AssistedInject constructor( - @Assisted val protocolInfo: Conversation.ProtocolInfo?, +class SearchAppsViewModel( + val protocolInfo: Conversation.ProtocolInfo?, private val getAllServices: ObserveAllServicesUseCase, private val syncServices: SyncServicesUseCase, private val getAllApps: ObserveAllAppsUseCase, @@ -130,11 +125,6 @@ class SearchAppsViewModel @AssistedInject constructor( ) } } - - @AssistedFactory - interface Factory { - fun create(protocolInfo: Conversation.ProtocolInfo?): SearchAppsViewModel - } } data class SearchServicesState( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelFactory.kt new file mode 100644 index 00000000000..de80911126a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelFactory.kt @@ -0,0 +1,53 @@ +/* + * 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.conversations.search.apps + +import com.wire.android.mapper.ContactMapper +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.feature.app.ObserveAllAppsUseCase +import com.wire.kalium.logic.feature.app.SearchAppsByNameUseCase +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 dev.zacsweers.metro.Inject + +@Inject +class SearchAppsViewModelFactory( + private val getAllServices: ObserveAllServicesUseCase, + private val syncServices: SyncServicesUseCase, + private val getAllApps: ObserveAllAppsUseCase, + private val contactMapper: ContactMapper, + private val searchServicesByName: SearchServicesByNameUseCase, + private val searchAppsByName: SearchAppsByNameUseCase, + private val isAppsAllowedForUsage: ObserveIsAppsAllowedForUsageUseCase, + private val observeSelfUser: ObserveSelfUserUseCase, +) { + fun create(protocolInfo: Conversation.ProtocolInfo?): SearchAppsViewModel = SearchAppsViewModel( + protocolInfo = protocolInfo, + getAllServices = getAllServices, + syncServices = syncServices, + getAllApps = getAllApps, + contactMapper = contactMapper, + searchServicesByName = searchServicesByName, + searchAppsByName = searchAppsByName, + isAppsAllowedForUsage = isAppsAllowedForUsage, + observeSelfUser = observeSelfUser, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt index 62127ab9473..d8dd42863f8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt @@ -28,11 +28,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.SearchScreenDestination import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.cells.ui.search.SearchNavArgs import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -59,7 +59,10 @@ import com.wire.android.ui.common.R as commonR @Composable fun SearchConversationMessagesScreen( navigator: Navigator, - searchConversationMessagesViewModel: SearchConversationMessagesViewModel = hiltViewModel() + navArgs: SearchConversationMessagesNavArgs, + searchConversationMessagesViewModel: SearchConversationMessagesViewModel = metroViewModel { + searchConversationMessagesViewModelFactory.create(navArgs) + } ) { SearchConversationMessagesResultContent( isCellsConversation = searchConversationMessagesViewModel.isCellsConversation, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt index 74ccec2b0b5..6d5c1ecc733 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt @@ -21,31 +21,24 @@ import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.ui.common.DEFAULT_SEARCH_QUERY_DEBOUNCE +import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.id.QualifiedID -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach -import javax.inject.Inject -@HiltViewModel -class SearchConversationMessagesViewModel @Inject constructor( +class SearchConversationMessagesViewModel( + searchConversationMessagesNavArgs: SearchConversationMessagesNavArgs, private val getSearchMessagesForConversation: GetConversationMessagesFromSearchUseCase, - private val dispatchers: DispatcherProvider, - savedStateHandle: SavedStateHandle + private val dispatchers: DispatcherProvider ) : ViewModel() { - private val searchConversationMessagesNavArgs: SearchConversationMessagesNavArgs = savedStateHandle.navArgs() - val conversationId: QualifiedID = searchConversationMessagesNavArgs.conversationId val groupName: String = searchConversationMessagesNavArgs.groupName val isCellsConversation: Boolean = searchConversationMessagesNavArgs.isCellsConversation diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModelFactory.kt new file mode 100644 index 00000000000..e83fa39590b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModelFactory.kt @@ -0,0 +1,35 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.conversations.search.messages + +import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import dev.zacsweers.metro.Inject + +@Inject +class SearchConversationMessagesViewModelFactory( + private val getSearchMessagesForConversation: GetConversationMessagesFromSearchUseCase, + private val dispatchers: DispatcherProvider, +) { + fun create(args: SearchConversationMessagesNavArgs): SearchConversationMessagesViewModel = + SearchConversationMessagesViewModel( + searchConversationMessagesNavArgs = args, + getSearchMessagesForConversation = getSearchMessagesForConversation, + dispatchers = dispatchers, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt index 8fdee9af436..9e4dfbe7794 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModel.kt @@ -21,10 +21,8 @@ package com.wire.android.ui.home.conversations.sendmessage import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.R import com.wire.android.appLogger import com.wire.android.feature.analytics.AnonymousAnalyticsManager @@ -75,19 +73,16 @@ import com.wire.kalium.logic.feature.message.SendLocationUseCase import com.wire.kalium.logic.feature.message.SendMultipartMessageUseCase import com.wire.kalium.logic.feature.message.SendTextMessageUseCase import com.wire.kalium.logic.feature.message.draft.RemoveMessageDraftUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class SendMessageViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class SendMessageViewModel( + private val conversationNavArgs: ConversationNavArgs, private val sendAssetMessage: ScheduleNewAssetMessageUseCase, private val sendTextMessage: SendTextMessageUseCase, private val sendMultipartMessage: SendMultipartMessageUseCase, @@ -112,7 +107,6 @@ class SendMessageViewModel @Inject constructor( private val sharedState: MessageSharedState ) : ViewModel() { - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId private val _infoMessage = MutableSharedFlow() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelFactory.kt new file mode 100644 index 00000000000..8a93dffe9a0 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelFactory.kt @@ -0,0 +1,96 @@ +/* + * 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.conversations.sendmessage + +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.media.PingRinger +import com.wire.android.ui.home.conversations.ConversationNavArgs +import com.wire.android.ui.home.conversations.MessageSharedState +import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase +import com.wire.android.util.ImageUtil +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.asset.KaliumFileSystem +import com.wire.kalium.logic.feature.asset.upload.ScheduleNewAssetMessageUseCase +import com.wire.kalium.logic.feature.client.IsWireCellsEnabledForConversationUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationUnderLegalHoldNotifiedUseCase +import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase +import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase +import com.wire.kalium.logic.feature.conversation.SetNotifiedAboutConversationUnderLegalHoldUseCase +import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase +import com.wire.kalium.logic.feature.message.RetryFailedMessageUseCase +import com.wire.kalium.logic.feature.message.SendEditMultipartMessageUseCase +import com.wire.kalium.logic.feature.message.SendEditTextMessageUseCase +import com.wire.kalium.logic.feature.message.SendKnockUseCase +import com.wire.kalium.logic.feature.message.SendLocationUseCase +import com.wire.kalium.logic.feature.message.SendMultipartMessageUseCase +import com.wire.kalium.logic.feature.message.SendTextMessageUseCase +import com.wire.kalium.logic.feature.message.draft.RemoveMessageDraftUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class SendMessageViewModelFactory( + private val sendAssetMessage: ScheduleNewAssetMessageUseCase, + private val sendTextMessage: SendTextMessageUseCase, + private val sendMultipartMessage: SendMultipartMessageUseCase, + private val sendEditTextMessage: SendEditTextMessageUseCase, + private val sendEditMultipartMessage: SendEditMultipartMessageUseCase, + private val retryFailedMessage: RetryFailedMessageUseCase, + private val dispatchers: DispatcherProvider, + private val kaliumFileSystem: KaliumFileSystem, + private val handleUriAsset: HandleUriAssetUseCase, + private val sendKnock: SendKnockUseCase, + private val sendTypingEvent: SendTypingEventUseCase, + private val pingRinger: PingRinger, + private val imageUtil: ImageUtil, + private val setUserInformedAboutVerification: SetUserInformedAboutVerificationUseCase, + private val observeDegradedConversationNotified: ObserveDegradedConversationNotifiedUseCase, + private val setNotifiedAboutConversationUnderLegalHold: SetNotifiedAboutConversationUnderLegalHoldUseCase, + private val observeConversationUnderLegalHoldNotified: ObserveConversationUnderLegalHoldNotifiedUseCase, + private val sendLocation: SendLocationUseCase, + private val removeMessageDraft: RemoveMessageDraftUseCase, + private val analyticsManager: AnonymousAnalyticsManager, + private val isWireCellsEnabledForConversation: IsWireCellsEnabledForConversationUseCase, + private val sharedState: MessageSharedState, +) { + fun create(conversationNavArgs: ConversationNavArgs): SendMessageViewModel = SendMessageViewModel( + conversationNavArgs = conversationNavArgs, + sendAssetMessage = sendAssetMessage, + sendTextMessage = sendTextMessage, + sendMultipartMessage = sendMultipartMessage, + sendEditTextMessage = sendEditTextMessage, + sendEditMultipartMessage = sendEditMultipartMessage, + retryFailedMessage = retryFailedMessage, + dispatchers = dispatchers, + kaliumFileSystem = kaliumFileSystem, + handleUriAsset = handleUriAsset, + sendKnock = sendKnock, + sendTypingEvent = sendTypingEvent, + pingRinger = pingRinger, + imageUtil = imageUtil, + setUserInformedAboutVerification = setUserInformedAboutVerification, + observeDegradedConversationNotified = observeDegradedConversationNotified, + setNotifiedAboutConversationUnderLegalHold = setNotifiedAboutConversationUnderLegalHold, + observeConversationUnderLegalHoldNotified = observeConversationUnderLegalHoldNotified, + sendLocation = sendLocation, + removeMessageDraft = removeMessageDraft, + analyticsManager = analyticsManager, + isWireCellsEnabledForConversation = isWireCellsEnabledForConversation, + sharedState = sharedState, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt index 6b4da0d246d..df876c6efec 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModel.kt @@ -22,15 +22,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.di.AssistedViewModelFactory import com.wire.android.di.ScopedArgs import com.wire.android.di.ViewModelScopedPreview import com.wire.android.ui.home.conversations.usecase.ObserveUsersTypingInConversationUseCase import com.wire.kalium.logic.data.id.QualifiedID -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -39,10 +34,9 @@ interface TypingIndicatorViewModel { fun state(): UsersTypingViewState = UsersTypingViewState() } -@HiltViewModel(assistedFactory = TypingIndicatorViewModelImpl.Factory::class) -class TypingIndicatorViewModelImpl @AssistedInject constructor( +class TypingIndicatorViewModelImpl( private val observeUsersTypingInConversation: ObserveUsersTypingInConversationUseCase, - @Assisted private val args: TypingIndicatorArgs, + private val args: TypingIndicatorArgs, ) : TypingIndicatorViewModel, ViewModel() { val conversationId: QualifiedID = args.conversationId @@ -60,11 +54,6 @@ class TypingIndicatorViewModelImpl @AssistedInject constructor( } } } - - @AssistedFactory - interface Factory : AssistedViewModelFactory { - override fun create(args: TypingIndicatorArgs): TypingIndicatorViewModelImpl - } } @Serializable diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModelFactory.kt new file mode 100644 index 00000000000..73a0a3e2ceb --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModelFactory.kt @@ -0,0 +1,31 @@ +/* + * 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.conversations.typing + +import com.wire.android.ui.home.conversations.usecase.ObserveUsersTypingInConversationUseCase +import dev.zacsweers.metro.Inject + +@Inject +class TypingIndicatorViewModelFactory( + private val observeUsersTypingInConversation: ObserveUsersTypingInConversationUseCase, +) { + fun create(args: TypingIndicatorArgs): TypingIndicatorViewModelImpl = TypingIndicatorViewModelImpl( + observeUsersTypingInConversation = observeUsersTypingInConversation, + args = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt index fab1e4ab1ab..686489b985d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetAssetMessagesFromConversationUseCase.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.flow.map import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime -import javax.inject.Inject +import dev.zacsweers.metro.Inject import kotlin.math.max class GetAssetMessagesFromConversationUseCase @Inject constructor( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt index cc40c6b6114..5bb1b8b4e73 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject +import dev.zacsweers.metro.Inject import kotlin.math.max class GetConversationMessagesFromSearchUseCase @Inject constructor( 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..26a23107167 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 @@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject +import dev.zacsweers.metro.Inject class GetConversationsFromSearchUseCase @Inject constructor( private val useCase: GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt index 12ea1ff312d..8c74c06f321 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetMessagesForConversationUseCase.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import java.lang.Integer.max -import javax.inject.Inject +import dev.zacsweers.metro.Inject class GetMessagesForConversationUseCase @Inject constructor( private val getMessages: GetPaginatedFlowOfMessagesByConversationUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCase.kt index b506dd50812..577e7454a1a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetQuoteMessageForConversationUseCase.kt @@ -28,7 +28,7 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase import kotlinx.coroutines.withContext -import javax.inject.Inject +import dev.zacsweers.metro.Inject class GetQuoteMessageForConversationUseCase @Inject constructor( private val getMessageById: GetMessageByIdUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCase.kt index 5d7e24b047d..b71688f643e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetUsersForMessageUseCase.kt @@ -25,7 +25,7 @@ import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.mapLatest -import javax.inject.Inject +import dev.zacsweers.metro.Inject class GetUsersForMessageUseCase @Inject constructor( private val observeMemberDetailsByIds: ObserveUserListByIdUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/HandleUriAssetUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/HandleUriAssetUseCase.kt index eb04dac45c4..3ddfa0cbd2c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/HandleUriAssetUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/HandleUriAssetUseCase.kt @@ -26,7 +26,7 @@ import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase import kotlinx.coroutines.withContext import java.util.UUID -import javax.inject.Inject +import dev.zacsweers.metro.Inject class HandleUriAssetUseCase @Inject constructor( private val getAssetSizeLimit: GetAssetSizeLimitUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveImageAssetMessagesFromConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveImageAssetMessagesFromConversationUseCase.kt index 3e4964c2229..55b85ce11cb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveImageAssetMessagesFromConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveImageAssetMessagesFromConversationUseCase.kt @@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.datetime.toLocalDateTime -import javax.inject.Inject +import dev.zacsweers.metro.Inject import kotlin.math.max class ObserveImageAssetMessagesFromConversationUseCase @Inject constructor( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveMessageForConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveMessageForConversationUseCase.kt index ac881359c94..43b111bfc1d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveMessageForConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveMessageForConversationUseCase.kt @@ -27,7 +27,7 @@ import com.wire.kalium.logic.feature.message.ObserveMessageByIdUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject +import dev.zacsweers.metro.Inject class ObserveMessageForConversationUseCase @Inject constructor( private val observeMessage: ObserveMessageByIdUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveQuoteMessageForConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveQuoteMessageForConversationUseCase.kt index 0fcf55319aa..4f93b06096e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveQuoteMessageForConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveQuoteMessageForConversationUseCase.kt @@ -29,7 +29,7 @@ import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.feature.message.ObserveMessageByIdUseCase import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import javax.inject.Inject +import dev.zacsweers.metro.Inject class ObserveQuoteMessageForConversationUseCase @Inject constructor( private val observeMessageById: ObserveMessageByIdUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveUsersTypingInConversationUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveUsersTypingInConversationUseCase.kt index 67204e29bd8..455fe3302ed 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveUsersTypingInConversationUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/ObserveUsersTypingInConversationUseCase.kt @@ -23,7 +23,7 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.conversation.ObserveUsersTypingUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject +import dev.zacsweers.metro.Inject class ObserveUsersTypingInConversationUseCase @Inject constructor( private val observeUsersTyping: ObserveUsersTypingUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListCallViewModel.kt index 0b8f837baf8..967db781449 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListCallViewModel.kt @@ -29,12 +29,10 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import javax.inject.Inject interface ConversationListCallViewModel : ActionsManager { val joinCallDialogState: VisibilityState get() = VisibilityState() @@ -45,8 +43,7 @@ interface ConversationListCallViewModel : ActionsManager() override val infoMessage = _infoMessage.asSharedFlow() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelFactory.kt new file mode 100644 index 00000000000..c28a2cbec97 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelFactory.kt @@ -0,0 +1,69 @@ +/* + * 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 + +import com.wire.android.BuildConfig +import com.wire.android.di.CurrentAccount +import com.wire.android.mapper.UserTypeMapper +import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer +import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase +import com.wire.android.ui.home.conversationslist.model.ConversationsSource +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.android.util.ui.UiTextResolver +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetailsWithEventsUseCase +import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase +import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class ConversationListViewModelFactory( + private val dispatcher: DispatcherProvider, + private val getConversationsPaginated: GetConversationsFromSearchUseCase, + private val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase, + private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, + private val refreshConversationsWithoutMetadata: RefreshConversationsWithoutMetadataUseCase, + private val observeLegalHoldStateForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, + private val audioMessagePlayer: ConversationAudioMessagePlayer, + @CurrentAccount private val currentAccount: UserId, + private val userTypeMapper: UserTypeMapper, + private val getSelfUser: GetSelfUserUseCase, + private val uiTextResolver: UiTextResolver, +) { + fun create( + conversationsSource: ConversationsSource, + usePagination: Boolean = BuildConfig.PAGINATED_CONVERSATION_LIST_ENABLED, + ): ConversationListViewModelImpl = ConversationListViewModelImpl( + conversationsSource = conversationsSource, + usePagination = usePagination, + dispatcher = dispatcher, + getConversationsPaginated = getConversationsPaginated, + observeConversationListDetailsWithEvents = observeConversationListDetailsWithEvents, + refreshUsersWithoutMetadata = refreshUsersWithoutMetadata, + refreshConversationsWithoutMetadata = refreshConversationsWithoutMetadata, + observeLegalHoldStateForSelfUser = observeLegalHoldStateForSelfUser, + audioMessagePlayer = audioMessagePlayer, + currentAccount = currentAccount, + userTypeMapper = userTypeMapper, + getSelfUser = getSelfUser, + uiTextResolver = uiTextResolver, + ) +} 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 78a699c3aea..948dd05f69d 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 @@ -28,7 +28,6 @@ import androidx.compose.runtime.remember 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.paging.LoadState import androidx.paging.compose.LazyPagingItems import com.ramcosta.composedestinations.generated.app.destinations.BrowseChannelsScreenDestination @@ -40,6 +39,7 @@ import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProf import com.ramcosta.composedestinations.generated.app.destinations.PromoteAdminScreenDestination import com.wire.android.R import com.wire.android.appLogger +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.navigation.NavigationCommand @@ -87,16 +87,15 @@ fun ConversationsScreenContent( conversationsSource: ConversationsSource = ConversationsSource.MAIN, conversationListViewModel: ConversationListViewModel = when { LocalInspectionMode.current -> ConversationListViewModelPreview() - else -> hiltViewModel( - key = "list_$conversationsSource", - creationCallback = { factory -> - factory.create(conversationsSource = conversationsSource) - } - ) + else -> metroViewModel(key = "list_$conversationsSource") { + conversationListViewModelFactory.create(conversationsSource = conversationsSource) + } }, conversationListCallViewModel: ConversationListCallViewModel = when { LocalInspectionMode.current -> ConversationListCallViewModelPreview - else -> hiltViewModel(key = "call_$conversationsSource") + else -> metroViewModel(key = "call_$conversationsSource") { + conversationListCallViewModelFactory.create() + } }, ) { val sheetState = rememberWireModalSheetState() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt index b3262ad6a71..6b34c35379b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/all/AllConversationsScreen.kt @@ -22,7 +22,7 @@ import com.wire.android.navigation.annotation.app.WireHomeDestination import androidx.compose.animation.Crossfade import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Composable -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.HomeDestination import com.wire.android.navigation.rememberNavigator import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout @@ -30,7 +30,6 @@ import com.wire.android.ui.common.search.rememberSearchbarState import com.wire.android.ui.home.HomeStateHolder import com.wire.android.ui.home.conversations.folder.ConversationFoldersStateArgs import com.wire.android.ui.home.conversations.folder.ConversationFoldersVM -import com.wire.android.ui.home.conversations.folder.ConversationFoldersVMImpl import com.wire.android.ui.home.conversationslist.ConversationListViewModelPreview import com.wire.android.ui.home.conversationslist.ConversationsScreenContent import com.wire.android.ui.home.conversationslist.common.previewConversationItemsFlow @@ -45,9 +44,7 @@ import com.wire.kalium.logic.data.conversation.ConversationFilter fun AllConversationsScreen( homeStateHolder: HomeStateHolder, foldersViewModel: ConversationFoldersVM = - hiltViewModel( - creationCallback = { it.create(ConversationFoldersStateArgs(null)) } - ), + metroViewModel { conversationFoldersViewModelFactory.create(ConversationFoldersStateArgs(null)) }, ) { with(homeStateHolder) { Crossfade( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModel.kt index 0d1fd0ee159..70ececfb092 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModel.kt @@ -21,7 +21,6 @@ package com.wire.android.ui.home.drawer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.BuildConfig @@ -33,18 +32,14 @@ import com.wire.kalium.logic.feature.conversation.ObserveArchivedUnreadConversat import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import dagger.Lazy -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("LongParameterList") -@HiltViewModel -class HomeDrawerViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class HomeDrawerViewModel( private val observeArchivedUnreadConversationsCount: Lazy, private val observeSelfUser: ObserveSelfUserUseCase, private val getTeamUrl: GetTeamUrlUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModelFactory.kt new file mode 100644 index 00000000000..39665c5d5ff --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModelFactory.kt @@ -0,0 +1,40 @@ +/* + * 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.drawer + +import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase +import com.wire.kalium.logic.feature.conversation.ObserveArchivedUnreadConversationsCountUseCase +import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import dagger.Lazy +import dev.zacsweers.metro.Inject + +@Inject +class HomeDrawerViewModelFactory( + private val observeArchivedUnreadConversationsCount: Lazy, + private val observeSelfUser: ObserveSelfUserUseCase, + private val getTeamUrl: GetTeamUrlUseCase, + private val isWireCellsEnabled: IsWireCellsEnabledUseCase, +) { + fun create(): HomeDrawerViewModel = HomeDrawerViewModel( + observeArchivedUnreadConversationsCount = observeArchivedUnreadConversationsCount, + observeSelfUser = observeSelfUser, + getTeamUrl = getTeamUrl, + isWireCellsEnabled = isWireCellsEnabled, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreen.kt index 44e8f6d56c4..d181b8fcda2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryScreen.kt @@ -32,10 +32,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import coil3.annotation.ExperimentalCoilApi import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkScreenDestination import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -77,8 +77,11 @@ import com.wire.android.util.openDownloadFolder fun MediaGalleryScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, + args: MediaGalleryNavArgs, modifier: Modifier = Modifier, - mediaGalleryViewModel: MediaGalleryViewModel = hiltViewModel() + mediaGalleryViewModel: MediaGalleryViewModel = metroViewModel { + mediaGalleryViewModelFactory.create(args) + } ) { val permissionPermanentlyDeniedDialogState = rememberVisibilityState() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModel.kt index 818c275701f..72622896f2d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModel.kt @@ -21,14 +21,12 @@ package com.wire.android.ui.home.gallery import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.model.ImageAsset import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.common.visbility.VisibilityState import com.wire.android.ui.home.conversations.MediaGallerySnackbarMessages import com.wire.android.ui.home.conversations.delete.DeleteMessageDialogState -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.FileManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase @@ -42,7 +40,6 @@ import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import com.wire.kalium.logic.feature.asset.MessageAssetResult.Success import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.message.DeleteMessageUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filterIsInstance @@ -50,12 +47,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okio.Path -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class MediaGalleryViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class MediaGalleryViewModel( + private val mediaGalleryNavArgs: MediaGalleryNavArgs, private val getConversationDetails: ObserveConversationDetailsUseCase, private val dispatchers: DispatcherProvider, private val getImageData: GetMessageAssetUseCase, @@ -65,8 +60,6 @@ class MediaGalleryViewModel @Inject constructor( private val getCellNode: GetCellFileUseCase, ) : ActionsViewModel() { - private val mediaGalleryNavArgs: MediaGalleryNavArgs = savedStateHandle.navArgs() - private val messageId = mediaGalleryNavArgs.messageId private val conversationId = mediaGalleryNavArgs.conversationId private val cellAssetId = mediaGalleryNavArgs.cellAssetId diff --git a/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelFactory.kt new file mode 100644 index 00000000000..437278b3599 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelFactory.kt @@ -0,0 +1,49 @@ +/* + * 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.gallery + +import com.wire.android.util.FileManager +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase +import com.wire.kalium.cells.domain.usecase.GetMessageAttachmentUseCase +import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.message.DeleteMessageUseCase +import dev.zacsweers.metro.Inject + +@Inject +class MediaGalleryViewModelFactory( + private val getConversationDetails: ObserveConversationDetailsUseCase, + private val dispatchers: DispatcherProvider, + private val getImageData: GetMessageAssetUseCase, + private val fileManager: FileManager, + private val deleteMessage: DeleteMessageUseCase, + private val getAttachment: GetMessageAttachmentUseCase, + private val getCellNode: GetCellFileUseCase, +) { + fun create(args: MediaGalleryNavArgs): MediaGalleryViewModel = MediaGalleryViewModel( + mediaGalleryNavArgs = args, + getConversationDetails = getConversationDetails, + dispatchers = dispatchers, + getImageData = getImageData, + fileManager = fileManager, + deleteMessage = deleteMessage, + getAttachment = getAttachment, + getCellNode = getCellNode, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposeActions.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposeActions.kt index 0f00dd74f6f..5878a54ee16 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposeActions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposeActions.kt @@ -31,13 +31,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.model.ClickBlockParams import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WireSecondaryIconButton import com.wire.android.ui.common.dimensions import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionArgs import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionViewModel +import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionViewModelFactory import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionViewModelImpl import com.wire.android.ui.home.messagecomposer.attachments.AdditionalOptionButton import com.wire.android.ui.home.messagecomposer.state.AdditionalOptionSelectItem @@ -245,11 +246,11 @@ fun SelfDeletingMessageAction( conversationId: ConversationId, onButtonClicked: (SelfDeletionTimer) -> Unit, viewModel: SelfDeletingMessageActionViewModel = - hiltViewModelScoped< + wireViewModelScoped< SelfDeletingMessageActionViewModelImpl, SelfDeletingMessageActionViewModel, SelfDeletingMessageActionArgs, - SelfDeletingMessageActionViewModelImpl.Factory + SelfDeletingMessageActionViewModelFactory >( SelfDeletingMessageActionArgs(conversationId = conversationId) ), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt index 75744573d2b..d101270b9d0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt @@ -55,7 +55,7 @@ import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.constraintlayout.compose.atMost import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.spacers.VerticalSpace @@ -67,6 +67,7 @@ import com.wire.android.ui.home.conversations.UsersTypingIndicatorForConversatio import com.wire.android.ui.home.conversations.messages.QuotedMessagePreview import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionArgs import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionViewModel +import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionViewModelFactory import com.wire.android.ui.home.messagecomposer.actions.SelfDeletingMessageActionViewModelImpl import com.wire.android.ui.home.messagecomposer.attachments.AdditionalOptionButton import com.wire.android.ui.home.messagecomposer.model.MessageComposition @@ -184,11 +185,11 @@ private fun InputContent( onPlusClick: () -> Unit, modifier: Modifier = Modifier, viewModel: SelfDeletingMessageActionViewModel = - hiltViewModelScoped< + wireViewModelScoped< SelfDeletingMessageActionViewModelImpl, SelfDeletingMessageActionViewModel, SelfDeletingMessageActionArgs, - SelfDeletingMessageActionViewModelImpl.Factory + SelfDeletingMessageActionViewModelFactory >( SelfDeletingMessageActionArgs(conversationId = conversationId) ), diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/actions/SelfDeletingMessageActionViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/actions/SelfDeletingMessageActionViewModel.kt index f6dadc7cb2d..4a235953c84 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/actions/SelfDeletingMessageActionViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/actions/SelfDeletingMessageActionViewModel.kt @@ -22,16 +22,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.di.AssistedViewModelFactory import com.wire.android.di.ViewModelScopedPreview import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch @@ -43,11 +38,10 @@ interface SelfDeletingMessageActionViewModel { } @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel(assistedFactory = SelfDeletingMessageActionViewModelImpl.Factory::class) -class SelfDeletingMessageActionViewModelImpl @AssistedInject constructor( +class SelfDeletingMessageActionViewModelImpl( private val dispatchers: DispatcherProvider, private val observeSelfDeletingMessages: ObserveSelfDeletionTimerSettingsForConversationUseCase, - @Assisted private val args: SelfDeletingMessageActionArgs, + private val args: SelfDeletingMessageActionArgs, ) : SelfDeletingMessageActionViewModel, ViewModel() { private val conversationId: QualifiedID = args.conversationId @@ -71,9 +65,4 @@ class SelfDeletingMessageActionViewModelImpl @AssistedInject constructor( } } } - - @AssistedFactory - interface Factory : AssistedViewModelFactory { - override fun create(args: SelfDeletingMessageActionArgs): SelfDeletingMessageActionViewModelImpl - } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/actions/SelfDeletingMessageActionViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/actions/SelfDeletingMessageActionViewModelFactory.kt new file mode 100644 index 00000000000..a5dc179d37e --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/actions/SelfDeletingMessageActionViewModelFactory.kt @@ -0,0 +1,34 @@ +/* + * 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.messagecomposer.actions + +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase +import dev.zacsweers.metro.Inject + +@Inject +class SelfDeletingMessageActionViewModelFactory( + private val dispatchers: DispatcherProvider, + private val observeSelfDeletingMessages: ObserveSelfDeletionTimerSettingsForConversationUseCase, +) { + fun create(args: SelfDeletingMessageActionArgs): SelfDeletingMessageActionViewModelImpl = SelfDeletingMessageActionViewModelImpl( + dispatchers = dispatchers, + observeSelfDeletingMessages = observeSelfDeletingMessages, + args = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/AdditionalOptionButton.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/AdditionalOptionButton.kt index 2f71c1b8217..b37bec9f0d5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/AdditionalOptionButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/AdditionalOptionButton.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.wire.android.R -import com.wire.android.di.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WireSecondaryIconButton import com.wire.android.ui.theme.WireTheme @@ -52,7 +52,7 @@ fun AdditionalOptionButton( onClick: () -> Unit, modifier: Modifier = Modifier, viewModel: IsFileSharingEnabledViewModel = - hiltViewModelScoped() + wireViewModelScoped() ) { var enableAgain by remember { mutableStateOf(true) } LaunchedEffect(enableAgain, block = { @@ -84,7 +84,11 @@ private const val BUTTON_CLICK_DELAY_MILLIS = 400L @Composable fun PreviewAdditionalOptionButtonUnSelected() { WireTheme { - AdditionalOptionButton(isSelected = false, onClick = {}) + AdditionalOptionButton( + isSelected = false, + onClick = {}, + viewModel = object : IsFileSharingEnabledViewModel {} + ) } } @@ -92,6 +96,10 @@ fun PreviewAdditionalOptionButtonUnSelected() { @Composable fun PreviewAdditionalOptionButtonSelected() { WireTheme { - AdditionalOptionButton(isSelected = true, onClick = {}) + AdditionalOptionButton( + isSelected = true, + onClick = {}, + viewModel = object : IsFileSharingEnabledViewModel {} + ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/IsFileSharingEnabledViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/IsFileSharingEnabledViewModel.kt index 83094ccc0fa..996d91ae9bb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/IsFileSharingEnabledViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/IsFileSharingEnabledViewModel.kt @@ -26,17 +26,14 @@ import androidx.lifecycle.viewModelScope import com.wire.android.di.ViewModelScopedPreview import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject @ViewModelScopedPreview interface IsFileSharingEnabledViewModel { fun isFileSharingEnabled(): Boolean = true } -@HiltViewModel -class IsFileSharingEnabledViewModelImpl @Inject constructor( +class IsFileSharingEnabledViewModelImpl( private val isFileSharingEnabledUseCase: IsFileSharingEnabledUseCase, ) : IsFileSharingEnabledViewModel, ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/IsFileSharingEnabledViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/IsFileSharingEnabledViewModelFactory.kt new file mode 100644 index 00000000000..d26b1c654db --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/IsFileSharingEnabledViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.messagecomposer.attachments + +import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase +import dev.zacsweers.metro.Inject + +@Inject +class IsFileSharingEnabledViewModelFactory( + private val isFileSharingEnabledUseCase: IsFileSharingEnabledUseCase, +) { + fun create(): IsFileSharingEnabledViewModelImpl = IsFileSharingEnabledViewModelImpl( + isFileSharingEnabledUseCase = isFileSharingEnabledUseCase, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelper.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelper.kt index 78fe7939e2f..e51b1d5b004 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelper.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/GeocoderHelper.kt @@ -19,7 +19,7 @@ package com.wire.android.ui.home.messagecomposer.location import android.location.Geocoder import android.location.Location -import javax.inject.Inject +import dev.zacsweers.metro.Inject class GeocoderHelper @Inject constructor(private val geocoder: Geocoder) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt index 331e0bd8160..7574b217abc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerComponent.kt @@ -42,8 +42,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.zIndex -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.common.bottomsheet.MenuItemIcon import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader import com.wire.android.ui.common.bottomsheet.WireMenuModalSheetContent @@ -71,7 +71,7 @@ import com.wire.android.ui.common.R as commonR fun LocationPickerComponent( onLocationPicked: (GeoLocatedAddress) -> Unit, modifier: Modifier = Modifier, - viewModel: LocationPickerViewModel = hiltViewModel(), + viewModel: LocationPickerViewModel = metroViewModel { locationPickerViewModelFactory.create() }, sheetState: WireModalSheetState = rememberWireModalSheetState(), ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt index b626619f0e7..7e6088dc1f3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelper.kt @@ -31,7 +31,8 @@ import com.wire.android.appLogger import com.wire.android.di.ApplicationScope import com.wire.android.ui.home.appLock.CurrentTimestampProvider import com.wire.kalium.logger.KaliumLogLevel -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext +import dev.zacsweers.metro.Inject as MetroInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.delay @@ -44,7 +45,7 @@ import kotlin.time.Duration.Companion.seconds @Suppress("TooGenericExceptionCaught") @SuppressLint("MissingPermission") -class LocationPickerHelper @Inject constructor( +class LocationPickerHelper @Inject @MetroInject constructor( @ApplicationContext private val context: Context, @ApplicationScope private val scope: CoroutineScope, private val currentTimestampProvider: CurrentTimestampProvider, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt index 2a8037aaf7e..737248ef16e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModel.kt @@ -22,12 +22,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class LocationPickerViewModel @Inject constructor(private val locationPickerHelper: LocationPickerHelperFlavor) : ViewModel() { +class LocationPickerViewModel(private val locationPickerHelper: LocationPickerHelperFlavor) : ViewModel() { var state: LocationPickerState by mutableStateOf(LocationPickerState()) private set diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelFactory.kt new file mode 100644 index 00000000000..5b1e698ac1f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelFactory.kt @@ -0,0 +1,29 @@ +/* + * 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.messagecomposer.location + +import dev.zacsweers.metro.Inject + +@Inject +class LocationPickerViewModelFactory( + private val locationPickerHelper: LocationPickerHelperFlavor, +) { + fun create(): LocationPickerViewModel = LocationPickerViewModel( + locationPickerHelper = locationPickerHelper, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt index 57fafe30245..ffba74d9488 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/AudioMediaRecorder.kt @@ -33,7 +33,6 @@ import com.wire.android.util.fileDateTime import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase.AssetSizeLimits.ASSET_SIZE_DEFAULT_LIMIT_BYTES import com.wire.kalium.util.DateTimeUtil -import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -54,10 +53,8 @@ import java.io.IOException import java.io.RandomAccessFile import java.nio.ByteBuffer import java.nio.ByteOrder -import javax.inject.Inject -@ViewModelScoped -class AudioMediaRecorder @Inject constructor( +class AudioMediaRecorder( private val kaliumFileSystem: KaliumFileSystem, private val dispatcherProvider: DispatcherProvider ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/GenerateAudioFileWithEffectsUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/GenerateAudioFileWithEffectsUseCase.kt index 454c0e5c52a..21227daac31 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/GenerateAudioFileWithEffectsUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/GenerateAudioFileWithEffectsUseCase.kt @@ -22,11 +22,12 @@ import com.waz.audioeffect.AudioEffect import com.wire.android.appLogger import com.wire.android.util.dispatchers.DispatcherProvider import kotlinx.coroutines.withContext +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject import javax.inject.Singleton @Singleton -class GenerateAudioFileWithEffectsUseCase @Inject constructor( +class GenerateAudioFileWithEffectsUseCase @Inject @MetroInject constructor( private val dispatchers: DispatcherProvider, ) { /** diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt index cf8d9b8eb00..a6fad2ed522 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner -import com.sebaslogen.resaca.hilt.hiltViewModelScoped +import com.wire.android.di.wireViewModelScoped import com.wire.android.ui.common.HandleActions import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions @@ -50,9 +50,13 @@ fun RecordAudioComponent( onAudioRecorded: (UriAsset) -> Unit, onCloseRecordAudio: () -> Unit, modifier: Modifier = Modifier, - lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + viewModel: RecordAudioViewModel = wireViewModelScoped< + RecordAudioViewModel, + RecordAudioViewModel, + RecordAudioViewModelFactory, + >() ) { - val viewModel: RecordAudioViewModel = hiltViewModelScoped() val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioFileGateway.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioFileGateway.kt new file mode 100644 index 00000000000..a2d9949af46 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioFileGateway.kt @@ -0,0 +1,62 @@ +/* + * 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.messagecomposer.recordaudio + +import android.content.Context +import android.net.Uri +import com.wire.android.util.SUPPORTED_AUDIO_MIME_TYPE +import com.wire.android.util.fromNioPathToContentUri +import com.wire.android.util.getAudioLengthInMs +import okio.Path +import java.io.File + +interface RecordAudioFileGateway { + suspend fun generateAudioFileWithEffects( + originalFilePath: String, + effectsFilePath: String + ) + + fun audioLengthInMs(audioPath: Path): Long + fun contentUri(audioFile: File): Uri +} + +class AndroidRecordAudioFileGateway( + private val context: Context, + private val generateAudioFileWithEffects: GenerateAudioFileWithEffectsUseCase, +) : RecordAudioFileGateway { + + override suspend fun generateAudioFileWithEffects( + originalFilePath: String, + effectsFilePath: String + ) { + generateAudioFileWithEffects( + context = context, + originalFilePath = originalFilePath, + effectsFilePath = effectsFilePath + ) + } + + override fun audioLengthInMs(audioPath: Path): Long = + getAudioLengthInMs( + dataPath = audioPath, + mimeType = SUPPORTED_AUDIO_MIME_TYPE + ) + + override fun contentUri(audioFile: File): Uri = + context.fromNioPathToContentUri(nioPath = audioFile.toPath()) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt index 96430531ecb..5ee62405257 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModel.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.home.messagecomposer.recordaudio -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -33,19 +32,14 @@ import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.home.conversations.model.UriAsset import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager -import com.wire.android.util.SUPPORTED_AUDIO_MIME_TYPE import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.fileDateTime -import com.wire.android.util.fromNioPathToContentUri -import com.wire.android.util.getAudioLengthInMs import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.feature.asset.AudioNormalizedLoudnessBuilder import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.util.DateTimeUtil -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -53,17 +47,14 @@ import kotlinx.coroutines.launch import okio.Path.Companion.toPath import java.io.File import java.io.IOException -import javax.inject.Inject import kotlin.io.path.deleteIfExists @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel -class RecordAudioViewModel @Inject constructor( - @ApplicationContext private val context: Context, +class RecordAudioViewModel( private val recordAudioMessagePlayer: RecordAudioMessagePlayer, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val getAssetSizeLimit: GetAssetSizeLimitUseCase, - private val generateAudioFileWithEffects: GenerateAudioFileWithEffectsUseCase, + private val recordAudioFileGateway: RecordAudioFileGateway, private val currentScreenManager: CurrentScreenManager, private val audioMediaRecorder: AudioMediaRecorder, private val globalDataStore: GlobalDataStore, @@ -201,8 +192,7 @@ class RecordAudioViewModel @Inject constructor( audioState = state.audioState.copy(audioMediaPlayingState = AudioMediaPlayingState.Fetching) ) if (state.shouldApplyEffects && state.effectsOutputFile != null) { - generateAudioFileWithEffects( - context = context, + recordAudioFileGateway.generateAudioFileWithEffects( originalFilePath = state.originalOutputFile!!.path, effectsFilePath = state.effectsOutputFile!!.path ) @@ -214,10 +204,7 @@ class RecordAudioViewModel @Inject constructor( audioState = AudioState.DEFAULT.copy( totalTimeInMs = AudioState.TotalTimeInMs.Known( playableAudioFile?.let { - getAudioLengthInMs( - dataPath = it.path.toPath(), - mimeType = SUPPORTED_AUDIO_MIME_TYPE - ).toInt() + recordAudioFileGateway.audioLengthInMs(it.path.toPath()).toInt() } ?: 0 ), ), @@ -320,12 +307,12 @@ class RecordAudioViewModel @Inject constructor( RecordAudioViewActions.Recorded( uriAsset = UriAsset( uri = if (didSucceed) { - context.fromNioPathToContentUri(nioPath = audioMediaRecorder.m4aOutputPath!!.toNioPath()) + recordAudioFileGateway.contentUri(audioMediaRecorder.m4aOutputPath!!.toFile()) } else { if (state.shouldApplyEffects) { - context.fromNioPathToContentUri(nioPath = state.effectsOutputFile!!.toPath()) + recordAudioFileGateway.contentUri(state.effectsOutputFile!!) } else { - context.fromNioPathToContentUri(nioPath = state.originalOutputFile!!.toPath()) + recordAudioFileGateway.contentUri(state.originalOutputFile!!) } }, mimeType = if (didSucceed) { @@ -371,8 +358,7 @@ class RecordAudioViewModel @Inject constructor( audioState = state.audioState.copy(audioMediaPlayingState = AudioMediaPlayingState.Fetching) ) - generateAudioFileWithEffects( - context = context, + recordAudioFileGateway.generateAudioFileWithEffects( originalFilePath = state.originalOutputFile!!.path, effectsFilePath = effectsFile.path ) @@ -384,10 +370,7 @@ class RecordAudioViewModel @Inject constructor( audioMediaPlayingState = AudioMediaPlayingState.Stopped, currentPositionInMs = 0, AudioState.TotalTimeInMs.Known( - getAudioLengthInMs( - dataPath = effectsFile.path.toPath(), - mimeType = SUPPORTED_AUDIO_MIME_TYPE - ).toInt() + recordAudioFileGateway.audioLengthInMs(effectsFile.path.toPath()).toInt() ), ), wavesMask = state.wavesMask, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelFactory.kt new file mode 100644 index 00000000000..40d435e531d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelFactory.kt @@ -0,0 +1,59 @@ +/* + * 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.messagecomposer.recordaudio + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.media.audiomessage.AudioFocusHelper +import com.wire.android.media.audiomessage.RecordAudioMessagePlayer +import com.wire.android.util.CurrentScreenManager +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.asset.KaliumFileSystem +import com.wire.kalium.logic.feature.asset.AudioNormalizedLoudnessBuilder +import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class RecordAudioViewModelFactory( + private val recordAudioMessagePlayer: RecordAudioMessagePlayer, + private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, + private val getAssetSizeLimit: GetAssetSizeLimitUseCase, + private val recordAudioFileGateway: RecordAudioFileGateway, + private val currentScreenManager: CurrentScreenManager, + private val audioMediaRecorder: AudioMediaRecorder, + private val globalDataStore: GlobalDataStore, + private val audioNormalizedLoudnessBuilder: AudioNormalizedLoudnessBuilder, + private val audioFocusHelper: AudioFocusHelper, + private val dispatchers: DispatcherProvider, + private val kaliumFileSystem: KaliumFileSystem, +) { + fun create(): RecordAudioViewModel = RecordAudioViewModel( + recordAudioMessagePlayer = recordAudioMessagePlayer, + observeEstablishedCalls = observeEstablishedCalls, + getAssetSizeLimit = getAssetSizeLimit, + recordAudioFileGateway = recordAudioFileGateway, + currentScreenManager = currentScreenManager, + audioMediaRecorder = audioMediaRecorder, + globalDataStore = globalDataStore, + audioNormalizedLoudnessBuilder = audioNormalizedLoudnessBuilder, + audioFocusHelper = audioFocusHelper, + dispatchers = dispatchers, + kaliumFileSystem = kaliumFileSystem, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt index 369ddcbc6a6..7d4769d8b50 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModel.kt @@ -51,16 +51,13 @@ import com.wire.kalium.logic.feature.featureConfig.AppsAllowedResult import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class NewConversationViewModel @Inject constructor( +class NewConversationViewModel( private val createRegularGroup: CreateRegularGroupUseCase, private val createChannel: CreateChannelUseCase, private val isUserAllowedToCreateChannels: ObserveChannelsCreationPermissionUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelFactory.kt new file mode 100644 index 00000000000..4a0beb0731c --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelFactory.kt @@ -0,0 +1,49 @@ +/* + * 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.newconversation + +import com.wire.kalium.logic.feature.channels.ObserveChannelsCreationPermissionUseCase +import com.wire.kalium.logic.feature.client.IsWireCellsEnabledUseCase +import com.wire.kalium.logic.feature.conversation.createconversation.CreateChannelUseCase +import com.wire.kalium.logic.feature.conversation.createconversation.CreateRegularGroupUseCase +import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class NewConversationViewModelFactory( + private val createRegularGroup: CreateRegularGroupUseCase, + private val createChannel: CreateChannelUseCase, + private val isUserAllowedToCreateChannels: ObserveChannelsCreationPermissionUseCase, + private val getSelfUser: GetSelfUserUseCase, + private val getDefaultProtocol: GetDefaultProtocolUseCase, + private val isWireCellsFeatureEnabled: IsWireCellsEnabledUseCase, + private val observeIsAppsAllowedForUsage: ObserveIsAppsAllowedForUsageUseCase, +) { + fun create(): NewConversationViewModel = NewConversationViewModel( + createRegularGroup = createRegularGroup, + createChannel = createChannel, + isUserAllowedToCreateChannels = isUserAllowedToCreateChannels, + getSelfUser = getSelfUser, + getDefaultProtocol = getDefaultProtocol, + isWireCellsFeatureEnabled = isWireCellsFeatureEnabled, + observeIsAppsAllowedForUsage = observeIsAppsAllowedForUsage, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt index 51df8b776da..bd2032da28e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsScreen.kt @@ -28,10 +28,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.appLogger +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.Clickable import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.HomeDestination @@ -50,7 +50,9 @@ import com.wire.android.util.ui.UIText @Composable fun SettingsScreen( homeStateHolder: HomeStateHolder, - viewModel: SettingsViewModel = hiltViewModel() + viewModel: SettingsViewModel = metroViewModel { + settingsViewModelFactory.create() + } ) { val turnAppLockOffDialogState = rememberVisibilityState() val onAppLockSwitchClicked: (Boolean) -> Unit = remember { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt index 81375aad0ad..aeab998db86 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModel.kt @@ -27,16 +27,13 @@ import com.wire.android.datastore.GlobalDataStore import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class SettingsViewModel @Inject constructor( +class SettingsViewModel( private val globalDataStore: GlobalDataStore, private val observeIsAppLockEditable: ObserveIsAppLockEditableUseCase, private val getSelf: ObserveSelfUserUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelFactory.kt new file mode 100644 index 00000000000..f0872b9c153 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelFactory.kt @@ -0,0 +1,39 @@ +/* + * 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.settings + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +class SettingsViewModelFactory( + private val globalDataStore: GlobalDataStore, + private val observeIsAppLockEditable: ObserveIsAppLockEditableUseCase, + private val getSelf: ObserveSelfUserUseCase, + private val dispatchers: DispatcherProvider, +) { + fun create(): SettingsViewModel = SettingsViewModel( + globalDataStore = globalDataStore, + observeIsAppLockEditable = observeIsAppLockEditable, + getSelf = getSelf, + dispatchers = dispatchers, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesInfoProvider.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesInfoProvider.kt new file mode 100644 index 00000000000..3dc8cc0c975 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesInfoProvider.kt @@ -0,0 +1,31 @@ +/* + * 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.settings.about.dependencies + +import android.content.Context +import com.wire.android.util.getDependenciesVersion + +interface DependenciesInfoProvider { + suspend fun dependenciesVersion(): Map +} + +class AndroidDependenciesInfoProvider( + private val context: Context +) : DependenciesInfoProvider { + override suspend fun dependenciesVersion(): Map = context.getDependenciesVersion() +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesScreen.kt index ef892abf118..5813c11bf5f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesScreen.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.ui.common.rowitem.RowItemTemplate import com.wire.android.ui.common.dimensions @@ -46,7 +46,9 @@ import kotlinx.collections.immutable.persistentMapOf @WireRootDestination fun DependenciesScreen( navigator: Navigator, - viewModel: DependenciesViewModel = hiltViewModel() + viewModel: DependenciesViewModel = metroViewModel { + dependenciesViewModelFactory.create() + } ) { WireScaffold(topBar = { WireCenterAlignedTopAppBar( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModel.kt index 21db51c43ff..e860c3b3170 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModel.kt @@ -17,22 +17,16 @@ */ package com.wire.android.ui.home.settings.about.dependencies -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.util.getDependenciesVersion -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class DependenciesViewModel @Inject constructor( - @ApplicationContext val context: Context +class DependenciesViewModel( + private val dependenciesInfoProvider: DependenciesInfoProvider ) : ViewModel() { var state: DependenciesState by mutableStateOf(DependenciesState()) @@ -44,7 +38,7 @@ class DependenciesViewModel @Inject constructor( private fun checkDependenciesVersion() { viewModelScope.launch { - val dependencies = context.getDependenciesVersion().toImmutableMap() + val dependencies = dependenciesInfoProvider.dependenciesVersion().toImmutableMap() state = state.copy(dependencies = dependencies) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModelFactory.kt new file mode 100644 index 00000000000..e489bb08c3a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModelFactory.kt @@ -0,0 +1,29 @@ +/* + * 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.settings.about.dependencies + +import dev.zacsweers.metro.Inject + +@Inject +class DependenciesViewModelFactory( + private val dependenciesInfoProvider: DependenciesInfoProvider, +) { + fun create(): DependenciesViewModel = DependenciesViewModel( + dependenciesInfoProvider = dependenciesInfoProvider, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesProvider.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesProvider.kt new file mode 100644 index 00000000000..c2d3cc33354 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesProvider.kt @@ -0,0 +1,42 @@ +/* + * 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.settings.about.licenses + +import android.content.Context +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.entity.Library +import com.mikepenz.aboutlibraries.util.withContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +interface LicensesProvider { + suspend fun getLibraries(): List +} + +class AndroidLicensesProvider( + private val context: Context +) : LicensesProvider { + + override suspend fun getLibraries(): List = withContext(Dispatchers.IO) { + Libs.Builder() + .withContext(context) + .build() + .libraries + .distinctBy { it.uniqueId } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesScreen.kt index d6869281f55..7093788d862 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesScreen.kt @@ -28,10 +28,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.mikepenz.aboutlibraries.entity.Library import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar @@ -40,7 +40,9 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar @Composable fun LicensesScreen( navigator: Navigator, - viewModel: LicensesViewModel = hiltViewModel() + viewModel: LicensesViewModel = metroViewModel { + licensesViewModelFactory.create() + } ) { WireScaffold(topBar = { WireCenterAlignedTopAppBar( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModel.kt index 8017285eded..0b3cef67c59 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModel.kt @@ -17,32 +17,23 @@ */ package com.wire.android.ui.home.settings.about.licenses -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.mikepenz.aboutlibraries.Libs -import com.mikepenz.aboutlibraries.util.withContext -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class LicensesViewModel @Inject constructor( - @ApplicationContext context: Context +class LicensesViewModel( + private val licensesProvider: LicensesProvider ) : ViewModel() { var state: LicensesState by mutableStateOf(LicensesState()) private set init { - viewModelScope.launch(Dispatchers.IO) { - val libraryList = Libs.Builder().withContext(context).build().libraries.distinctBy { it.uniqueId } - state = state.copy(libraryList = libraryList) + viewModelScope.launch { + state = state.copy(libraryList = licensesProvider.getLibraries()) } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModelFactory.kt new file mode 100644 index 00000000000..3a0101fc208 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModelFactory.kt @@ -0,0 +1,29 @@ +/* + * 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.settings.about.licenses + +import dev.zacsweers.metro.Inject + +@Inject +class LicensesViewModelFactory( + private val licensesProvider: LicensesProvider, +) { + fun create(): LicensesViewModel = LicensesViewModel( + licensesProvider = licensesProvider, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt index 676eb31b371..dc696c9add8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountScreen.kt @@ -42,10 +42,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import com.ramcosta.composedestinations.spec.DestinationSpec +import com.wire.android.di.metro.metroViewModel import com.wire.android.R import com.wire.android.appLogger import com.wire.android.model.Clickable @@ -94,8 +94,8 @@ fun MyAccountScreen( changeDisplayNameResultRecipient: ResultRecipient, changeHandleResultRecipient: ResultRecipient, changeUserColorResultRecipient: ResultRecipient, - viewModel: MyAccountViewModel = hiltViewModel(), - deleteAccountViewModel: DeleteAccountViewModel = hiltViewModel() + viewModel: MyAccountViewModel = metroViewModel { myAccountViewModelFactory.create() }, + deleteAccountViewModel: DeleteAccountViewModel = metroViewModel { deleteAccountViewModelFactory.create() } ) { val snackbarHostState = LocalSnackbarHostState.current val scope = rememberCoroutineScope() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt index 6601fcf882d..7e0c948ec8d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModel.kt @@ -35,7 +35,6 @@ import com.wire.kalium.logic.feature.user.IsSelfATeamMemberUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn @@ -44,12 +43,10 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import javax.inject.Inject import kotlin.properties.Delegates @Suppress("LongParameterList") -@HiltViewModel -class MyAccountViewModel @Inject constructor( +class MyAccountViewModel( private val getSelf: ObserveSelfUserUseCase, private val observeSelfUserWithTeam: ObserveSelfUserWithTeamUseCase, private val isSelfATeamMember: IsSelfATeamMemberUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModelFactory.kt new file mode 100644 index 00000000000..698d01ba918 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModelFactory.kt @@ -0,0 +1,52 @@ +/* + * 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.settings.account + +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase +import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase +import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase +import com.wire.kalium.logic.feature.user.IsSelfATeamMemberUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase +import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class MyAccountViewModelFactory( + private val getSelf: ObserveSelfUserUseCase, + private val observeSelfUserWithTeam: ObserveSelfUserWithTeamUseCase, + private val isSelfATeamMember: IsSelfATeamMemberUseCase, + private val serverConfig: SelfServerConfigUseCase, + private val isPasswordRequired: IsPasswordRequiredUseCase, + private val isReadOnlyAccount: IsReadOnlyAccountUseCase, + private val dispatchers: DispatcherProvider, + private val isE2EIEnabledUseCase: IsE2EIEnabledUseCase, +) { + fun create(): MyAccountViewModel = MyAccountViewModel( + getSelf = getSelf, + observeSelfUserWithTeam = observeSelfUserWithTeam, + isSelfATeamMember = isSelfATeamMember, + serverConfig = serverConfig, + isPasswordRequired = isPasswordRequired, + isReadOnlyAccount = isReadOnlyAccount, + dispatchers = dispatchers, + isE2EIEnabledUseCase = isE2EIEnabledUseCase, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorScreen.kt index ba370248d7e..4d586638749 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorScreen.kt @@ -36,12 +36,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator -import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.BuildConfig.IS_BUBBLE_UI_ENABLED import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator +import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.ui.common.HandleActions import com.wire.android.ui.common.R as commonR import com.wire.android.ui.common.WireDropDown @@ -83,7 +83,7 @@ import com.wire.kalium.logic.data.id.QualifiedID fun ChangeUserColorScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, - viewModel: ChangeUserColorViewModel = hiltViewModel() + viewModel: ChangeUserColorViewModel = metroViewModel { changeUserColorViewModelFactory.create() } ) { with(viewModel) { ChangeUserColorContent( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorViewModel.kt index 519236eb3f0..76d72841ba4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorViewModel.kt @@ -27,12 +27,9 @@ import com.wire.android.ui.theme.Accent import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.UpdateAccentColorResult import com.wire.kalium.logic.feature.user.UpdateAccentColorUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class ChangeUserColorViewModel @Inject constructor( +class ChangeUserColorViewModel( private val getSelf: GetSelfUserUseCase, private val updateAccentColor: UpdateAccentColorUseCase, ) : ActionsViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorViewModelFactory.kt new file mode 100644 index 00000000000..ca6fab95b59 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorViewModelFactory.kt @@ -0,0 +1,33 @@ +/* + * 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.settings.account.color + +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.UpdateAccentColorUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ChangeUserColorViewModelFactory( + private val getSelf: GetSelfUserUseCase, + private val updateAccentColor: UpdateAccentColorUseCase, +) { + fun create(): ChangeUserColorViewModel = ChangeUserColorViewModel( + getSelf = getSelf, + updateAccentColor = updateAccentColor, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/deleteAccount/DeleteAccountViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/deleteAccount/DeleteAccountViewModel.kt index b78c3478e35..214df373b6c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/deleteAccount/DeleteAccountViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/deleteAccount/DeleteAccountViewModel.kt @@ -23,12 +23,9 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.kalium.logic.feature.user.DeleteAccountUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class DeleteAccountViewModel @Inject constructor( +class DeleteAccountViewModel( private val deleteAccount: DeleteAccountUseCase, ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/deleteAccount/DeleteAccountViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/deleteAccount/DeleteAccountViewModelFactory.kt new file mode 100644 index 00000000000..b458a5dfe9c --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/deleteAccount/DeleteAccountViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.settings.account.deleteAccount + +import com.wire.kalium.logic.feature.user.DeleteAccountUseCase +import dev.zacsweers.metro.Inject + +@Inject +class DeleteAccountViewModelFactory( + private val deleteAccount: DeleteAccountUseCase, +) { + fun create(): DeleteAccountViewModel = DeleteAccountViewModel( + deleteAccount = deleteAccount, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameScreen.kt index d59c1f64b35..8928dfa4e48 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameScreen.kt @@ -41,10 +41,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.DisplayNameState import com.wire.android.navigation.Navigator import com.wire.android.ui.common.R as commonR @@ -74,7 +74,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes fun ChangeDisplayNameScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, - viewModel: ChangeDisplayNameViewModel = hiltViewModel() + viewModel: ChangeDisplayNameViewModel = metroViewModel { changeDisplayNameViewModelFactory.create() } ) { with(viewModel) { LaunchedEffect(viewModel.displayNameState.completed) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameViewModel.kt index 8b0a17cfa29..5ef2c264ea1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameViewModel.kt @@ -30,13 +30,10 @@ import com.wire.android.ui.common.textfield.textAsFlow import com.wire.kalium.logic.feature.user.DisplayNameUpdateResult import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.UpdateDisplayNameUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class ChangeDisplayNameViewModel @Inject constructor( +class ChangeDisplayNameViewModel( private val getSelf: GetSelfUserUseCase, private val updateDisplayName: UpdateDisplayNameUseCase, ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameViewModelFactory.kt new file mode 100644 index 00000000000..2b11e0e27a6 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameViewModelFactory.kt @@ -0,0 +1,33 @@ +/* + * 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.settings.account.displayname + +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.UpdateDisplayNameUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ChangeDisplayNameViewModelFactory( + private val getSelf: GetSelfUserUseCase, + private val updateDisplayName: UpdateDisplayNameUseCase, +) { + fun create(): ChangeDisplayNameViewModel = ChangeDisplayNameViewModel( + getSelf = getSelf, + updateDisplayName = updateDisplayName, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailScreen.kt index a88aab43b97..52792974278 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailScreen.kt @@ -38,9 +38,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -69,7 +69,7 @@ import com.wire.android.ui.common.R as commonR @Composable fun ChangeEmailScreen( navigator: Navigator, - viewModel: ChangeEmailViewModel = hiltViewModel() + viewModel: ChangeEmailViewModel = metroViewModel { changeEmailViewModelFactory.create() } ) { when (val flowState = viewModel.state.flowState) { is ChangeEmailState.FlowState.NoChange, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModel.kt index 67801f027f1..9bffcba2bd9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModel.kt @@ -29,13 +29,10 @@ import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.util.Patterns import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.UpdateEmailUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class ChangeEmailViewModel @Inject constructor( +class ChangeEmailViewModel( private val updateEmail: UpdateEmailUseCase, private val getSelf: GetSelfUserUseCase, ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModelFactory.kt new file mode 100644 index 00000000000..fa6cdf32c92 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModelFactory.kt @@ -0,0 +1,33 @@ +/* + * 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.settings.account.email.updateEmail + +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.UpdateEmailUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ChangeEmailViewModelFactory( + private val updateEmail: UpdateEmailUseCase, + private val getSelf: GetSelfUserUseCase, +) { + fun create(): ChangeEmailViewModel = ChangeEmailViewModel( + updateEmail = updateEmail, + getSelf = getSelf, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailScreen.kt index c6b2ede2e40..a3710664b88 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailScreen.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.ui.common.button.WireButtonState.Default import com.wire.android.ui.common.button.WireButtonState.Disabled @@ -57,7 +57,8 @@ import com.wire.android.util.ui.stringWithStyledArgs @Composable fun VerifyEmailScreen( navigator: Navigator, - viewModel: VerifyEmailViewModel = hiltViewModel() + args: VerifyEmailNavArgs, + viewModel: VerifyEmailViewModel = metroViewModel { verifyEmailViewModelFactory.create(args) } ) { LaunchedEffect(viewModel.state.noChange) { if (viewModel.state.noChange) navigator.navigateBack() diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailViewModel.kt index 95dd1a9f747..0c1342b7af3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailViewModel.kt @@ -20,25 +20,19 @@ package com.wire.android.ui.home.settings.account.email.verifyEmail import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.feature.user.UpdateEmailUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class VerifyEmailViewModel @Inject constructor( +class VerifyEmailViewModel( private val updateEmail: UpdateEmailUseCase, - savedStateHandle: SavedStateHandle + private val verifyEmailNavArgs: VerifyEmailNavArgs ) : ViewModel() { var state: VerifyEmailState by mutableStateOf(VerifyEmailState()) private set - private val verifyEmailNavArgs: VerifyEmailNavArgs = savedStateHandle.navArgs() val newEmail: String = verifyEmailNavArgs.newEmail fun onResendVerificationEmailClicked() { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailViewModelFactory.kt new file mode 100644 index 00000000000..1ca363dea67 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailViewModelFactory.kt @@ -0,0 +1,31 @@ +/* + * 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.settings.account.email.verifyEmail + +import com.wire.kalium.logic.feature.user.UpdateEmailUseCase +import dev.zacsweers.metro.Inject + +@Inject +class VerifyEmailViewModelFactory( + private val updateEmail: UpdateEmailUseCase, +) { + fun create(args: VerifyEmailNavArgs): VerifyEmailViewModel = VerifyEmailViewModel( + updateEmail = updateEmail, + verifyEmailNavArgs = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleScreen.kt index 640735e7b7f..0c57c4c1ae6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleScreen.kt @@ -36,8 +36,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.R import com.wire.android.navigation.Navigator @@ -63,7 +63,7 @@ import com.wire.android.ui.common.R as commonR fun ChangeHandleScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, - viewModel: ChangeHandleViewModel = hiltViewModel() + viewModel: ChangeHandleViewModel = metroViewModel { changeHandleViewModelFactory.create() } ) { LaunchedEffect(viewModel.state.isSuccess) { if (viewModel.state.isSuccess) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModel.kt index e775311a2a5..a232bedc5f7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModel.kt @@ -32,13 +32,10 @@ import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.SetUserHandleResult import com.wire.kalium.logic.feature.user.SetUserHandleUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class ChangeHandleViewModel @Inject constructor( +class ChangeHandleViewModel( private val updateHandle: SetUserHandleUseCase, private val validateHandle: ValidateUserHandleUseCase, private val getSelf: GetSelfUserUseCase diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModelFactory.kt new file mode 100644 index 00000000000..e5e3739b7b0 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModelFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.settings.account.handle + +import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import com.wire.kalium.logic.feature.user.SetUserHandleUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ChangeHandleViewModelFactory( + private val updateHandle: SetUserHandleUseCase, + private val validateHandle: ValidateUserHandleUseCase, + private val getSelf: GetSelfUserUseCase, +) { + fun create(): ChangeHandleViewModel = ChangeHandleViewModel( + updateHandle = updateHandle, + validateHandle = validateHandle, + getSelf = getSelf, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationScreen.kt index 7bec5b0c582..df7c07150db 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationScreen.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.scaffold.WireScaffold @@ -63,7 +63,9 @@ import com.wire.android.util.ui.UIText @Composable fun CustomizationScreen( navigator: Navigator, - viewModel: CustomizationViewModel = hiltViewModel() + viewModel: CustomizationViewModel = metroViewModel { + customizationViewModelFactory.create() + } ) { val lazyListState: LazyListState = rememberLazyListState() CustomizationScreenContent( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationViewModel.kt index 0557bc0a4ae..572146a0fea 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationViewModel.kt @@ -25,12 +25,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.datastore.GlobalDataStore import com.wire.android.ui.theme.ThemeOption -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CustomizationViewModel @Inject constructor( +class CustomizationViewModel( private val globalDataStore: GlobalDataStore, ) : ViewModel() { var state by mutableStateOf(CustomizationState()) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationViewModelFactory.kt new file mode 100644 index 00000000000..c609031a4a7 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.settings.appearance + +import com.wire.android.datastore.GlobalDataStore +import dev.zacsweers.metro.Inject + +@Inject +class CustomizationViewModelFactory( + private val globalDataStore: GlobalDataStore, +) { + fun create(): CustomizationViewModel = CustomizationViewModel( + globalDataStore = globalDataStore, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsDefaultsProvider.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsDefaultsProvider.kt new file mode 100644 index 00000000000..8ba62a0e197 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsDefaultsProvider.kt @@ -0,0 +1,34 @@ +/* + * 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.settings.appsettings.networkSettings + +import android.content.Context +import com.wire.android.util.isWebsocketEnabledByDefault + +interface NetworkSettingsDefaultsProvider { + val isWebSocketEnabledByDefault: Boolean +} + +class AndroidNetworkSettingsDefaultsProvider( + private val context: Context +) : NetworkSettingsDefaultsProvider { + + override val isWebSocketEnabledByDefault: Boolean + get() = isWebsocketEnabledByDefault(context) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt index 95831c4e309..5bd5c51db3b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.preview.MultipleThemePreviews @@ -42,7 +42,9 @@ import com.wire.android.ui.theme.WireTheme @Composable fun NetworkSettingsScreen( navigator: Navigator, - networkSettingsViewModel: NetworkSettingsViewModel = hiltViewModel() + networkSettingsViewModel: NetworkSettingsViewModel = metroViewModel { + networkSettingsViewModelFactory.create() + } ) { NetworkSettingsScreenContent( onBackPressed = navigator::navigateBack, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModel.kt index 323f81b55f7..a674a47d382 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModel.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home.settings.appsettings.networkSettings -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -26,24 +25,18 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.emm.ManagedConfigurationsManager -import com.wire.android.util.isWebsocketEnabledByDefault import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import com.wire.kalium.logic.feature.user.webSocketStatus.PersistPersistentWebSocketConnectionStatusUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class NetworkSettingsViewModel -@Inject constructor( +class NetworkSettingsViewModel( private val persistPersistentWebSocketConnectionStatus: PersistPersistentWebSocketConnectionStatusUseCase, private val observePersistentWebSocketConnectionStatus: ObservePersistentWebSocketConnectionStatusUseCase, private val currentSession: CurrentSessionUseCase, private val managedConfigurationsManager: ManagedConfigurationsManager, - @ApplicationContext private val context: Context + private val networkSettingsDefaultsProvider: NetworkSettingsDefaultsProvider ) : ViewModel() { var networkSettingsState by mutableStateOf(NetworkSettingsState()) @@ -55,7 +48,7 @@ class NetworkSettingsViewModel private fun checkWebSocketEnforcedByDefault() { networkSettingsState = networkSettingsState.copy( - isWebSocketEnforcedByDefault = isWebsocketEnabledByDefault(context) + isWebSocketEnforcedByDefault = networkSettingsDefaultsProvider.isWebSocketEnabledByDefault ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModelFactory.kt new file mode 100644 index 00000000000..eb2197d9709 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModelFactory.kt @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.settings.appsettings.networkSettings + +import com.wire.android.emm.ManagedConfigurationsManager +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase +import com.wire.kalium.logic.feature.user.webSocketStatus.PersistPersistentWebSocketConnectionStatusUseCase +import dev.zacsweers.metro.Inject + +@Inject +class NetworkSettingsViewModelFactory( + private val persistPersistentWebSocketConnectionStatus: PersistPersistentWebSocketConnectionStatusUseCase, + private val observePersistentWebSocketConnectionStatus: ObservePersistentWebSocketConnectionStatusUseCase, + private val currentSession: CurrentSessionUseCase, + private val managedConfigurationsManager: ManagedConfigurationsManager, + private val networkSettingsDefaultsProvider: NetworkSettingsDefaultsProvider, +) { + fun create(): NetworkSettingsViewModel = NetworkSettingsViewModel( + persistPersistentWebSocketConnectionStatus = persistPersistentWebSocketConnectionStatus, + observePersistentWebSocketConnectionStatus = observePersistentWebSocketConnectionStatus, + currentSession = currentSession, + managedConfigurationsManager = managedConfigurationsManager, + networkSettingsDefaultsProvider = networkSettingsDefaultsProvider, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreScreen.kt index a5076747ade..e9be5bd5e2e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreScreen.kt @@ -38,8 +38,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -64,16 +64,18 @@ import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun BackupAndRestoreScreen( navigator: Navigator, - viewModel: BackupAndRestoreViewModel = hiltViewModel() + viewModel: BackupAndRestoreViewModel = metroViewModel { + backupAndRestoreViewModelFactory.create() + } ) { BackupAndRestoreContent( backUpAndRestoreState = viewModel.state, createBackupPasswordTextState = viewModel.createBackupPasswordState, restoreBackupPasswordTextState = viewModel.restoreBackupPasswordState, onCreateBackup = viewModel::createBackup, - onSaveBackup = viewModel::saveBackup, + onSaveBackup = { viewModel.saveBackup(it.toString()) }, onShareBackup = viewModel::shareBackup, - onChooseBackupFile = viewModel::chooseBackupFileToRestore, + onChooseBackupFile = { viewModel.chooseBackupFileToRestore(it.toString()) }, onRestoreBackup = viewModel::restorePasswordProtectedBackup, onCancelBackupRestore = viewModel::cancelBackupRestore, onCancelBackupCreation = viewModel::cancelBackupCreation, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt index 088af0c8de8..a16bfb3377e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home.settings.backup -import android.net.Uri import androidx.annotation.VisibleForTesting import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText @@ -32,9 +31,7 @@ import com.wire.android.datastore.UserDataStore import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.ui.common.textfield.textAsFlow -import com.wire.android.util.FileManager import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.feature.auth.ValidatePasswordResult import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import com.wire.kalium.logic.feature.backup.BackupFileFormat @@ -52,26 +49,22 @@ import com.wire.kalium.logic.feature.backup.RestoreMPBackupUseCase import com.wire.kalium.logic.feature.backup.VerifyBackupResult import com.wire.kalium.logic.feature.backup.VerifyBackupUseCase import com.wire.kalium.util.DateTimeUtil -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okio.Path -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class BackupAndRestoreViewModel @Inject constructor( +class BackupAndRestoreViewModel( private val importBackup: RestoreBackupUseCase, private val importMpBackup: RestoreMPBackupUseCase, private val createBackupFile: CreateBackupUseCase, private val createMpBackupFile: CreateMPBackupUseCase, private val verifyBackup: VerifyBackupUseCase, private val validatePassword: ValidatePasswordUseCase, - private val kaliumFileSystem: KaliumFileSystem, - private val fileManager: FileManager, + private val backupFileGateway: BackupFileGateway, private val userDataStore: UserDataStore, private val dispatcher: DispatcherProvider, private val mpBackupSettings: MPBackupSettings, @@ -154,9 +147,7 @@ class BackupAndRestoreViewModel @Inject constructor( fun shareBackup() = viewModelScope.launch { updateLastBackupDate() latestCreatedBackup?.let { backupData -> - withContext(dispatcher.io()) { - fileManager.shareWithExternalApp(backupData.path, backupData.assetName) {} - } + backupFileGateway.shareBackup(backupData.path, backupData.assetName) } state = state.copy( backupRestoreProgress = BackupRestoreProgress.InProgress(), @@ -167,10 +158,10 @@ class BackupAndRestoreViewModel @Inject constructor( ) } - fun saveBackup(uri: Uri) = viewModelScope.launch { + fun saveBackup(destinationUri: String) = viewModelScope.launch { updateLastBackupDate() latestCreatedBackup?.let { backupData -> - fileManager.copyToUri(backupData.path, uri, dispatcher) + backupFileGateway.saveBackup(backupData.path, destinationUri) } state = state.copy( backupRestoreProgress = BackupRestoreProgress.InProgress(), @@ -181,9 +172,8 @@ class BackupAndRestoreViewModel @Inject constructor( ) } - fun chooseBackupFileToRestore(uri: Uri) = viewModelScope.launch { - latestImportedBackupTempPath = kaliumFileSystem.tempFilePath(TEMP_IMPORTED_BACKUP_FILE_NAME) - fileManager.copyToPath(uri, latestImportedBackupTempPath) + fun chooseBackupFileToRestore(uri: String) = viewModelScope.launch { + latestImportedBackupTempPath = backupFileGateway.importBackupToTempPath(uri) verifyBackupFile(latestImportedBackupTempPath) } @@ -294,11 +284,8 @@ class BackupAndRestoreViewModel @Inject constructor( restorePasswordValidation = PasswordValidation.NotVerified ) withContext(dispatcher.io()) { - if (this@BackupAndRestoreViewModel::latestImportedBackupTempPath.isInitialized && kaliumFileSystem.exists( - latestImportedBackupTempPath - ) - ) { - kaliumFileSystem.delete(latestImportedBackupTempPath) + if (this@BackupAndRestoreViewModel::latestImportedBackupTempPath.isInitialized) { + backupFileGateway.deleteImportedBackup(latestImportedBackupTempPath) } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModelFactory.kt new file mode 100644 index 00000000000..2f9e278d16a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModelFactory.kt @@ -0,0 +1,56 @@ +/* + * 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.settings.backup + +import com.wire.android.datastore.UserDataStore +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase +import com.wire.kalium.logic.feature.backup.CreateBackupUseCase +import com.wire.kalium.logic.feature.backup.CreateMPBackupUseCase +import com.wire.kalium.logic.feature.backup.RestoreBackupUseCase +import com.wire.kalium.logic.feature.backup.RestoreMPBackupUseCase +import com.wire.kalium.logic.feature.backup.VerifyBackupUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class BackupAndRestoreViewModelFactory( + private val importBackup: RestoreBackupUseCase, + private val importMpBackup: RestoreMPBackupUseCase, + private val createBackupFile: CreateBackupUseCase, + private val createMpBackupFile: CreateMPBackupUseCase, + private val verifyBackup: VerifyBackupUseCase, + private val validatePassword: ValidatePasswordUseCase, + private val backupFileGateway: BackupFileGateway, + private val userDataStore: UserDataStore, + private val dispatcher: DispatcherProvider, + private val mpBackupSettings: MPBackupSettings, +) { + fun create(): BackupAndRestoreViewModel = BackupAndRestoreViewModel( + importBackup = importBackup, + importMpBackup = importMpBackup, + createBackupFile = createBackupFile, + createMpBackupFile = createMpBackupFile, + verifyBackup = verifyBackup, + validatePassword = validatePassword, + backupFileGateway = backupFileGateway, + userDataStore = userDataStore, + dispatcher = dispatcher, + mpBackupSettings = mpBackupSettings, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupFileGateway.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupFileGateway.kt new file mode 100644 index 00000000000..8d5ce22a3d9 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupFileGateway.kt @@ -0,0 +1,63 @@ +/* + * 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.settings.backup + +import androidx.core.net.toUri +import com.wire.android.util.FileManager +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.asset.KaliumFileSystem +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.withContext +import okio.Path + +interface BackupFileGateway { + suspend fun shareBackup(path: Path, assetName: String?) + suspend fun saveBackup(path: Path, destinationUri: String) + suspend fun importBackupToTempPath(sourceUri: String): Path + suspend fun deleteImportedBackup(path: Path) +} + +class AndroidBackupFileGateway @Inject constructor( + private val fileManager: FileManager, + private val kaliumFileSystem: KaliumFileSystem, + private val dispatcher: DispatcherProvider, +) : BackupFileGateway { + + override suspend fun shareBackup(path: Path, assetName: String?) = withContext(dispatcher.io()) { + fileManager.shareWithExternalApp(path, assetName) {} + } + + override suspend fun saveBackup(path: Path, destinationUri: String) { + fileManager.copyToUri(path, destinationUri.toUri(), dispatcher) + } + + override suspend fun importBackupToTempPath(sourceUri: String): Path { + val importedBackupPath = kaliumFileSystem.tempFilePath( + BackupAndRestoreViewModel.TEMP_IMPORTED_BACKUP_FILE_NAME + ) + fileManager.copyToPath(sourceUri.toUri(), importedBackupPath, dispatcher) + return importedBackupPath + } + + override suspend fun deleteImportedBackup(path: Path) = withContext(dispatcher.io()) { + if (kaliumFileSystem.exists(path)) { + kaliumFileSystem.delete(path) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsScreen.kt index 06283fbbb8a..41339025973 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsScreen.kt @@ -25,8 +25,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions @@ -43,7 +43,7 @@ import com.wire.android.ui.theme.WireTheme @Composable fun PrivacySettingsConfigScreen( navigator: Navigator, - viewModel: PrivacySettingsViewModel = hiltViewModel() + viewModel: PrivacySettingsViewModel = metroViewModel { privacySettingsViewModelFactory.create() } ) { with(viewModel) { PrivacySettingsScreenContent( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt index 75523967dde..2b46233f4e5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModel.kt @@ -39,15 +39,12 @@ import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotC import com.wire.kalium.logic.feature.user.typingIndicator.ObserveTypingIndicatorEnabledUseCase import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicatorStatusConfigUseCase import com.wire.kalium.logic.feature.user.typingIndicator.TypingIndicatorConfigResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject @Suppress("LongParameterList") -@HiltViewModel -class PrivacySettingsViewModel @Inject constructor( +class PrivacySettingsViewModel( private val dispatchers: DispatcherProvider, private val persistReadReceiptsStatusConfig: PersistReadReceiptsStatusConfigUseCase, private val observeReadReceiptsEnabled: ObserveReadReceiptsEnabledUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModelFactory.kt new file mode 100644 index 00000000000..79da192d437 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModelFactory.kt @@ -0,0 +1,58 @@ +/* + * 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.settings.privacy + +import com.wire.android.datastore.UserDataStore +import com.wire.android.ui.analytics.AnalyticsConfiguration +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase +import com.wire.kalium.logic.feature.user.readReceipts.ObserveReadReceiptsEnabledUseCase +import com.wire.kalium.logic.feature.user.readReceipts.PersistReadReceiptsStatusConfigUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase +import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCase +import com.wire.kalium.logic.feature.user.typingIndicator.ObserveTypingIndicatorEnabledUseCase +import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicatorStatusConfigUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class PrivacySettingsViewModelFactory( + private val dispatchers: DispatcherProvider, + private val persistReadReceiptsStatusConfig: PersistReadReceiptsStatusConfigUseCase, + private val observeReadReceiptsEnabled: ObserveReadReceiptsEnabledUseCase, + private val persistScreenshotCensoringConfig: PersistScreenshotCensoringConfigUseCase, + private val observeScreenshotCensoringConfig: ObserveScreenshotCensoringConfigUseCase, + private val persistTypingIndicatorStatusConfig: PersistTypingIndicatorStatusConfigUseCase, + private val observeTypingIndicatorEnabled: ObserveTypingIndicatorEnabledUseCase, + private val analyticsEnabled: AnalyticsConfiguration, + private val selfServerConfig: SelfServerConfigUseCase, + private val dataStore: UserDataStore, +) { + fun create(): PrivacySettingsViewModel = PrivacySettingsViewModel( + dispatchers = dispatchers, + persistReadReceiptsStatusConfig = persistReadReceiptsStatusConfig, + observeReadReceiptsEnabled = observeReadReceiptsEnabled, + persistScreenshotCensoringConfig = persistScreenshotCensoringConfig, + observeScreenshotCensoringConfig = observeScreenshotCensoringConfig, + persistTypingIndicatorStatusConfig = persistTypingIndicatorStatusConfig, + observeTypingIndicatorEnabled = observeTypingIndicatorEnabled, + analyticsEnabled = analyticsEnabled, + selfServerConfig = selfServerConfig, + dataStore = dataStore, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index ba620ca2198..9f8d50b779a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -41,18 +41,15 @@ import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.E2EIRequiredResult import dagger.Lazy -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("TooManyFunctions") -@HiltViewModel -class FeatureFlagNotificationViewModel @Inject constructor( +class FeatureFlagNotificationViewModel( @KaliumCoreLogic private val coreLogic: Lazy, private val currentSessionFlow: Lazy, private val globalDataStore: Lazy, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelFactory.kt new file mode 100644 index 00000000000..b4d62ae3c60 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelFactory.kt @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.sync + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.feature.DisableAppLockUseCase +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase +import dagger.Lazy +import dev.zacsweers.metro.Inject + +@Inject +class FeatureFlagNotificationViewModelFactory( + @KaliumCoreLogic private val coreLogic: Lazy, + private val currentSessionFlow: Lazy, + private val globalDataStore: Lazy, + private val disableAppLockUseCase: Lazy, +) { + fun create(): FeatureFlagNotificationViewModel = FeatureFlagNotificationViewModel( + coreLogic = coreLogic, + currentSessionFlow = currentSessionFlow, + globalDataStore = globalDataStore, + disableAppLockUseCase = disableAppLockUseCase, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/ReleaseNotesFeedUrlProvider.kt b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/ReleaseNotesFeedUrlProvider.kt new file mode 100644 index 00000000000..00c0bef54db --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/ReleaseNotesFeedUrlProvider.kt @@ -0,0 +1,32 @@ +/* + * 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.whatsnew + +import android.content.Context +import com.wire.android.R + +interface ReleaseNotesFeedUrlProvider { + val feedUrl: String +} + +class AndroidReleaseNotesFeedUrlProvider( + private val context: Context +) : ReleaseNotesFeedUrlProvider { + override val feedUrl: String + get() = context.resources.getString(R.string.url_android_release_notes_feed) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt index e5037907f39..015115d5f73 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt @@ -30,9 +30,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.BuildConfig import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.Clickable import com.wire.android.navigation.HomeDestination import com.wire.android.navigation.NavigationCommand @@ -45,7 +45,9 @@ import com.wire.android.util.ui.UIText @Composable fun WhatsNewScreen( homeStateHolder: HomeStateHolder, - whatsNewViewModel: WhatsNewViewModel = hiltViewModel() + whatsNewViewModel: WhatsNewViewModel = metroViewModel { + whatsNewViewModelFactory.create() + } ) { val context = LocalContext.current WhatsNewScreenContent( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModel.kt index 05c12cb9c9e..588b3a2dc45 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModel.kt @@ -17,23 +17,20 @@ */ package com.wire.android.ui.home.whatsnew -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.prof18.rssparser.RssParser -import com.wire.android.R import com.wire.android.util.toMediumOnlyDateTime -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.Locale -import javax.inject.Inject -@HiltViewModel -class WhatsNewViewModel @Inject constructor(context: Context) : ViewModel() { +class WhatsNewViewModel( + private val releaseNotesFeedUrlProvider: ReleaseNotesFeedUrlProvider +) : ViewModel() { private val rssParser = RssParser() private val publishDateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.ENGLISH) @@ -43,7 +40,7 @@ class WhatsNewViewModel @Inject constructor(context: Context) : ViewModel() { init { @Suppress("TooGenericExceptionCaught") viewModelScope.launch { - val feedUrl = context.resources.getString(R.string.url_android_release_notes_feed) + val feedUrl = releaseNotesFeedUrlProvider.feedUrl val items = try { if (feedUrl.isNotBlank()) { rssParser.getRssChannel(feedUrl).items diff --git a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelFactory.kt new file mode 100644 index 00000000000..9e7e923ea30 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelFactory.kt @@ -0,0 +1,29 @@ +/* + * 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.whatsnew + +import dev.zacsweers.metro.Inject + +@Inject +class WhatsNewViewModelFactory( + private val releaseNotesFeedUrlProvider: ReleaseNotesFeedUrlProvider, +) { + fun create(): WhatsNewViewModel = WhatsNewViewModel( + releaseNotesFeedUrlProvider = releaseNotesFeedUrlProvider, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncScreen.kt b/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncScreen.kt index 16614ff7a4a..f8ea8ee3eda 100644 --- a/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncScreen.kt @@ -20,9 +20,9 @@ package com.wire.android.ui.initialsync import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.lifecycleScope import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -38,7 +38,9 @@ import kotlinx.coroutines.launch @Composable fun InitialSyncScreen( navigator: Navigator, - viewModel: InitialSyncViewModel = hiltViewModel() + viewModel: InitialSyncViewModel = metroViewModel { + initialSyncViewModelFactory.create() + } ) { val activity = LocalActivity.current val syncCompletionState = viewModel.syncCompletionState diff --git a/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncViewModel.kt index 2afbfc35958..708df647b31 100644 --- a/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncViewModel.kt @@ -32,15 +32,12 @@ import com.wire.android.util.lifecycle.AutomatedLoginManager import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.sync.ObserveSyncStateUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel -class InitialSyncViewModel @Inject constructor( +class InitialSyncViewModel( private val observeSyncState: ObserveSyncStateUseCase, private val userDataStoreProvider: UserDataStoreProvider, @CurrentAccount private val userId: UserId, diff --git a/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncViewModelFactory.kt new file mode 100644 index 00000000000..3b331b325b7 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncViewModelFactory.kt @@ -0,0 +1,43 @@ +/* + * 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.initialsync + +import com.wire.android.datastore.UserDataStoreProvider +import com.wire.android.di.CurrentAccount +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.android.util.lifecycle.AutomatedLoginManager +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.sync.ObserveSyncStateUseCase +import dev.zacsweers.metro.Inject + +@Inject +class InitialSyncViewModelFactory( + private val observeSyncState: ObserveSyncStateUseCase, + private val userDataStoreProvider: UserDataStoreProvider, + @CurrentAccount private val userId: UserId, + private val dispatchers: DispatcherProvider, + private val automatedLoginManager: AutomatedLoginManager, +) { + fun create(): InitialSyncViewModel = InitialSyncViewModel( + observeSyncState = observeSyncState, + userDataStoreProvider = userDataStoreProvider, + userId = userId, + dispatchers = dispatchers, + automatedLoginManager = automatedLoginManager, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaCodeViewModel.kt index 880e46fd5b8..bb13d952607 100644 --- a/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaCodeViewModel.kt @@ -26,14 +26,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.ui.common.textfield.textAsFlow import com.wire.kalium.logic.feature.conversation.JoinConversationViaCodeUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class JoinConversationViaCodeViewModel @Inject constructor( +class JoinConversationViaCodeViewModel( private val joinViaCode: JoinConversationViaCodeUseCase ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaCodeViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaCodeViewModelFactory.kt new file mode 100644 index 00000000000..4516f52e44d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaCodeViewModelFactory.kt @@ -0,0 +1,30 @@ +/* + * 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.joinConversation + +import com.wire.kalium.logic.feature.conversation.JoinConversationViaCodeUseCase +import dev.zacsweers.metro.Inject + +@Inject +class JoinConversationViaCodeViewModelFactory( + private val joinViaCode: JoinConversationViaCodeUseCase, +) { + fun create(): JoinConversationViaCodeViewModel = JoinConversationViaCodeViewModel( + joinViaCode = joinViaCode, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaDeepLinkDialog.kt b/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaDeepLinkDialog.kt index a422ff65659..59fedea158e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaDeepLinkDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaDeepLinkDialog.kt @@ -36,8 +36,8 @@ import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction -import com.sebaslogen.resaca.hilt.hiltViewModelScoped import com.wire.android.R +import com.wire.android.di.wireViewModelScoped import com.wire.android.ui.common.WireDialog import com.wire.android.ui.common.WireDialogButtonProperties import com.wire.android.ui.common.WireDialogButtonType @@ -79,7 +79,8 @@ fun JoinConversationViaDeepLinkDialog( onFlowCompleted: (conversationId: ConversationId?) -> Unit, modifier: Modifier = Modifier, ) { - val viewModel = hiltViewModelScoped() + val viewModel: JoinConversationViaCodeViewModel = + wireViewModelScoped() val isLoading: Boolean by remember { derivedStateOf { viewModel.state is JoinViaDeepLinkDialogState.Loading } diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/deactivated/LegalHoldDeactivatedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/deactivated/LegalHoldDeactivatedViewModel.kt index 4772de0a0c5..515ade62ca9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/deactivated/LegalHoldDeactivatedViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/deactivated/LegalHoldDeactivatedViewModel.kt @@ -32,17 +32,14 @@ import com.wire.kalium.logic.feature.legalhold.MarkLegalHoldChangeAsNotifiedForS import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldChangeNotifiedForSelfUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import dagger.Lazy -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class LegalHoldDeactivatedViewModel @Inject constructor( +class LegalHoldDeactivatedViewModel( @KaliumCoreLogic private val coreLogic: Lazy ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/deactivated/LegalHoldDeactivatedViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/deactivated/LegalHoldDeactivatedViewModelFactory.kt new file mode 100644 index 00000000000..9ad7ea3c578 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/deactivated/LegalHoldDeactivatedViewModelFactory.kt @@ -0,0 +1,32 @@ +/* + * 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.legalhold.dialog.deactivated + +import com.wire.android.di.KaliumCoreLogic +import com.wire.kalium.logic.CoreLogic +import dagger.Lazy +import dev.zacsweers.metro.Inject + +@Inject +class LegalHoldDeactivatedViewModelFactory( + @KaliumCoreLogic private val coreLogic: Lazy, +) { + fun create(): LegalHoldDeactivatedViewModel = LegalHoldDeactivatedViewModel( + coreLogic = coreLogic, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt index e6d2325e790..08f394ac005 100644 --- a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModel.kt @@ -36,7 +36,6 @@ import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldRequestUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import dagger.Lazy -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest @@ -46,10 +45,8 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class LegalHoldRequestedViewModel @Inject constructor( +class LegalHoldRequestedViewModel( private val validatePassword: ValidatePasswordUseCase, @KaliumCoreLogic private val coreLogic: Lazy ) : ViewModel() { diff --git a/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelFactory.kt new file mode 100644 index 00000000000..e13dab421df --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelFactory.kt @@ -0,0 +1,35 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.legalhold.dialog.requested + +import com.wire.android.di.KaliumCoreLogic +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase +import dagger.Lazy +import dev.zacsweers.metro.Inject + +@Inject +class LegalHoldRequestedViewModelFactory( + private val validatePassword: ValidatePasswordUseCase, + @KaliumCoreLogic private val coreLogic: Lazy, +) { + fun create(): LegalHoldRequestedViewModel = LegalHoldRequestedViewModel( + validatePassword = validatePassword, + coreLogic = coreLogic, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/code/NewLoginVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/code/NewLoginVerificationCodeScreen.kt index 802fce00250..fdd49142547 100644 --- a/app/src/main/kotlin/com/wire/android/ui/newauthentication/code/NewLoginVerificationCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/code/NewLoginVerificationCodeScreen.kt @@ -57,7 +57,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun NewLoginVerificationCodeScreen( navigator: Navigator, - loginEmailViewModel: LoginEmailViewModel, // provided in MainNavHost to reuse from NewLoginPasswordScreen, don't use hiltViewModel() + loginEmailViewModel: LoginEmailViewModel, // provided in MainNavHost to reuse from NewLoginPasswordScreen ) { clearAutofillTree() LoginStateNavigationAndDialogs(loginEmailViewModel, navigator) diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/LoginFlowStateHolder.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/LoginFlowStateHolder.kt new file mode 100644 index 00000000000..0ba6985f973 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/LoginFlowStateHolder.kt @@ -0,0 +1,84 @@ +/* + * 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.newauthentication.login + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +data class LoginFlowHolderState( + val userIdentifier: String = "", + val flowState: NewLoginFlowState = NewLoginFlowState.Default, +) { + val nextEnabled: Boolean = flowState !is NewLoginFlowState.Loading && userIdentifier.isNotEmpty() +} + +class LoginFlowStateHolder( + initialUserIdentifier: String = "", + initialFlowState: NewLoginFlowState = NewLoginFlowState.Default, +) { + private val _state = MutableStateFlow( + LoginFlowHolderState( + userIdentifier = initialUserIdentifier, + flowState = initialFlowState, + ) + ) + val state: StateFlow = _state.asStateFlow() + + private val _results = MutableSharedFlow(extraBufferCapacity = 1) + val results: SharedFlow = _results.asSharedFlow() + + val userIdentifier: String + get() = _state.value.userIdentifier + + val flowState: NewLoginFlowState + get() = _state.value.flowState + + fun updateUserIdentifier(userIdentifier: String) { + _state.update { currentState -> + currentState.copy( + userIdentifier = userIdentifier, + flowState = currentState.flowState.resetTextFieldError(), + ) + } + } + + fun updateFlowState(flowState: NewLoginFlowState) { + updateFlowState { flowState } + } + + fun updateFlowState(update: (NewLoginFlowState) -> NewLoginFlowState) { + _state.update { currentState -> + currentState.copy(flowState = update(currentState.flowState)) + } + } + + fun tryEmitResult(result: Result): Boolean = _results.tryEmit(result) + + suspend fun emitResult(result: Result) { + _results.emit(result) + } + + private fun NewLoginFlowState.resetTextFieldError(): NewLoginFlowState = + if (this is NewLoginFlowState.Error.TextFieldError) NewLoginFlowState.Default else this +} diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/LoginNavigator.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/LoginNavigator.kt new file mode 100644 index 00000000000..c6cba26541d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/LoginNavigator.kt @@ -0,0 +1,51 @@ +/* + * 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.newauthentication.login + +import com.wire.android.ui.authentication.login.LoginPasswordPath +import com.wire.android.ui.authentication.login.sso.SSOUrlConfig +import com.wire.kalium.logic.configuration.server.ServerConfig + +fun interface LoginNavigator { + fun navigate(command: LoginNavigationCommand) +} + +sealed interface LoginNavigationCommand { + data class EnterpriseLoginNotSupported(val userIdentifier: String) : LoginNavigationCommand + data class EmailPassword(val userIdentifier: String, val loginPasswordPath: LoginPasswordPath) : LoginNavigationCommand + data class CustomConfig(val userIdentifier: String, val customServerConfig: ServerConfig.Links) : LoginNavigationCommand + data class SSO(val url: String, val config: SSOUrlConfig) : LoginNavigationCommand + data class Success(val nextStep: NextStep) : LoginNavigationCommand { + enum class NextStep { E2EIEnrollment, InitialSync, TooManyDevices, None } + } +} + +fun NewLoginAction.toLoginNavigationCommand(): LoginNavigationCommand = when (this) { + is NewLoginAction.EnterpriseLoginNotSupported -> LoginNavigationCommand.EnterpriseLoginNotSupported(userIdentifier) + is NewLoginAction.EmailPassword -> LoginNavigationCommand.EmailPassword(userIdentifier, loginPasswordPath) + is NewLoginAction.CustomConfig -> LoginNavigationCommand.CustomConfig(userIdentifier, customServerConfig) + is NewLoginAction.SSO -> LoginNavigationCommand.SSO(url, config) + is NewLoginAction.Success -> LoginNavigationCommand.Success(nextStep.toLoginNavigationCommandNextStep()) +} + +private fun NewLoginAction.Success.NextStep.toLoginNavigationCommandNextStep(): LoginNavigationCommand.Success.NextStep = when (this) { + NewLoginAction.Success.NextStep.E2EIEnrollment -> LoginNavigationCommand.Success.NextStep.E2EIEnrollment + NewLoginAction.Success.NextStep.InitialSync -> LoginNavigationCommand.Success.NextStep.InitialSync + NewLoginAction.Success.NextStep.TooManyDevices -> LoginNavigationCommand.Success.NextStep.TooManyDevices + NewLoginAction.Success.NextStep.None -> LoginNavigationCommand.Success.NextStep.None +} diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginNavArgsProvider.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginNavArgsProvider.kt new file mode 100644 index 00000000000..8af07ab711c --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginNavArgsProvider.kt @@ -0,0 +1,23 @@ +/* + * 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.newauthentication.login + +import javax.inject.Inject + +class NewLoginNavArgsProvider @Inject constructor() diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginRecoverableLogoutExceptionDetector.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginRecoverableLogoutExceptionDetector.kt new file mode 100644 index 00000000000..08588ad841d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginRecoverableLogoutExceptionDetector.kt @@ -0,0 +1,28 @@ +/* + * 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.newauthentication.login + +import android.database.sqlite.SQLiteException +import java.io.IOException +import javax.inject.Inject + +class NewLoginRecoverableLogoutExceptionDetector @Inject constructor() { + fun isRecoverableLogoutInterruption(exception: Exception): Boolean = + exception is IllegalStateException || exception is IOException || exception is SQLiteException +} diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginScreen.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginScreen.kt index d104958e2ad..2f3c18b2bf0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginScreen.kt @@ -47,7 +47,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.generated.app.destinations.E2EIEnrollmentScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.InitialSyncScreenDestination @@ -56,7 +55,9 @@ import com.ramcosta.composedestinations.generated.app.destinations.NewLoginPassw import com.ramcosta.composedestinations.generated.app.destinations.NewLoginScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.RemoveDeviceScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.WelcomeScreenDestination +import com.ramcosta.composedestinations.spec.Direction import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -69,7 +70,6 @@ import com.wire.android.ui.authentication.login.LoginPasswordPath import com.wire.android.ui.authentication.login.PreFilledUserIdentifierType import com.wire.android.ui.authentication.login.WireAuthBackgroundLayout import com.wire.android.ui.authentication.login.toLoginDialogErrorData -import com.ramcosta.composedestinations.spec.Direction import com.wire.android.ui.common.HandleActions import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WirePrimaryButton @@ -87,6 +87,8 @@ import com.wire.android.ui.theme.WireTheme import com.wire.android.util.CustomTabsHelper import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.configuration.server.ServerConfig +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.serialization.json.Json @WireNewLoginDestination( start = true, @@ -97,7 +99,9 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun NewLoginScreen( navigator: Navigator, navArgs: LoginNavArgs, - viewModel: NewLoginViewModel = hiltViewModel() + viewModel: NewLoginViewModel = metroViewModel { + newLoginViewModelFactory.create(navArgs, provideLoginSavedInputStore()) + } ) { val context = LocalContext.current val currentKeyboardController by rememberUpdatedState(LocalSoftwareKeyboardController.current) @@ -140,8 +144,15 @@ fun NewLoginScreen( } LaunchedEffect(Unit) { - navigator.navController.currentBackStackEntry?.savedStateHandle - ?.let { viewModel.observeSSOResult(it) } + val backStackSavedState = navigator.navController.currentBackStackEntry?.savedStateHandle + ?: return@LaunchedEffect + backStackSavedState + .getStateFlow(NewLoginViewModel.SSO_LOGIN_RESULT_KEY, null) + .filterNotNull() + .collect { json -> + viewModel.handleSSOResult(Json.decodeFromString(json)) + backStackSavedState.remove(NewLoginViewModel.SSO_LOGIN_RESULT_KEY) + } } // Handle SSO code auto-login from intent parameter diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt index a6c7a055207..837db15de22 100644 --- a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt @@ -18,27 +18,22 @@ package com.wire.android.ui.newauthentication.login -import android.database.sqlite.SQLiteException import androidx.annotation.VisibleForTesting import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.appLogger import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.ClientScopeProvider -import com.wire.android.di.DefaultWebSocketEnabledByDefault -import com.wire.android.di.KaliumCoreLogic import com.wire.android.ui.authentication.login.DomainClaimedByOrg import com.wire.android.ui.authentication.login.LoginNavArgs import com.wire.android.ui.authentication.login.LoginPasswordPath +import com.wire.android.ui.authentication.login.LoginSavedInputStore import com.wire.android.ui.authentication.login.LoginViewModelExtension import com.wire.android.ui.authentication.login.PreFilledUserIdentifierType -import com.wire.android.ui.authentication.login.email.LoginEmailViewModel.Companion.USER_IDENTIFIER_SAVED_STATE_KEY import com.wire.android.ui.authentication.login.sso.LoginSSOViewModelExtension import com.wire.android.ui.authentication.login.sso.SSOUrlConfig import com.wire.android.ui.authentication.login.sso.ssoCodeWithPrefix @@ -62,25 +57,19 @@ import com.wire.kalium.logic.feature.auth.sso.SSOLoginSessionResult import com.wire.kalium.logic.feature.backup.RestoreCryptoStateResult import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.session.DoesValidSessionExistResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import java.io.IOException -import javax.inject.Inject -import javax.inject.Named @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel class NewLoginViewModel( + private val loginNavArgs: LoginNavArgs, private val validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase, val coreLogic: CoreLogic, - savedStateHandle: SavedStateHandle, + private val savedInputStore: LoginSavedInputStore, val clientScopeProviderFactory: ClientScopeProvider.Factory, val userDataStoreProvider: UserDataStoreProvider, private val loginExtension: LoginViewModelExtension, @@ -88,34 +77,10 @@ class NewLoginViewModel( private val dispatchers: DispatcherProvider, defaultServerConfig: ServerConfig.Links, defaultSSOCodeConfig: String, + private val recoverableLogoutExceptionDetector: NewLoginRecoverableLogoutExceptionDetector, + private val sharedAuthNewLoginAdapter: SharedAuthNewLoginAdapter = LegacySharedAuthNewLoginAdapter, ) : ActionsViewModel() { - @Inject - constructor( - validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase, - @KaliumCoreLogic coreLogic: CoreLogic, - savedStateHandle: SavedStateHandle, - addAuthenticatedUser: AddAuthenticatedUserUseCase, - clientScopeProviderFactory: ClientScopeProvider.Factory, - userDataStoreProvider: UserDataStoreProvider, - dispatchers: DispatcherProvider, - defaultServerConfig: ServerConfig.Links, - @Named("ssoCodeConfig") defaultSSOCodeConfig: String, - @DefaultWebSocketEnabledByDefault defaultWebSocketEnabledByDefault: Boolean, - ) : this( - validateEmailOrSSOCode, - coreLogic, - savedStateHandle, - clientScopeProviderFactory, - userDataStoreProvider, - LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), - LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic, defaultWebSocketEnabledByDefault), - dispatchers, - defaultServerConfig, - defaultSSOCodeConfig - ) - - private val loginNavArgs: LoginNavArgs = savedStateHandle.navArgs() private val preFilledUserIdentifier: PreFilledUserIdentifierType = loginNavArgs.userHandle ?: PreFilledUserIdentifierType.None private var pendingNomadServiceUrl: String? = loginNavArgs.ssoCodeAutoLogin?.nomadServiceUrl private var pendingCookieLabel: String? = loginNavArgs.ssoCodeAutoLogin?.cookieLabel @@ -134,12 +99,12 @@ class NewLoginViewModel( } else if (defaultSSOCodeConfig.isNotEmpty() && !isCustomServerDeepLink) { defaultSSOCodeConfig.ssoCodeWithPrefix() } else { - savedStateHandle[USER_IDENTIFIER_SAVED_STATE_KEY] ?: String.EMPTY + savedInputStore.userIdentifier ?: String.EMPTY } ) viewModelScope.launch { userIdentifierTextState.textAsFlow().distinctUntilChanged().onEach { - savedStateHandle[USER_IDENTIFIER_SAVED_STATE_KEY] = it.toString() + savedInputStore.userIdentifier = it.toString() }.collectLatest { getAndUpdateLoginFlowState { currentState: NewLoginFlowState -> if (currentState is NewLoginFlowState.Error.TextFieldError) NewLoginFlowState.Default else currentState @@ -164,7 +129,7 @@ class NewLoginViewModel( appLogger.d("$TAG Successfully fetched default SSO code") withContext(dispatchers.main()) { userIdentifierTextState.setTextAndPlaceCursorAtEnd(defaultSSOCode) - savedStateHandle[USER_IDENTIFIER_SAVED_STATE_KEY] = defaultSSOCode + savedInputStore.userIdentifier = defaultSSOCode } } else { appLogger.d("$TAG No default SSO code configured for this server") @@ -182,6 +147,7 @@ class NewLoginViewModel( viewModelScope.launch(dispatchers.io()) { updateLoginFlowState(NewLoginFlowState.Loading) val sanitizedInput = userIdentifierTextState.text.trim().toString() + if (tryStartSharedAuthLogin(sanitizedInput)) return@launch when (validateEmailOrSSOCode(sanitizedInput)) { ValidateEmailOrSSOCodeUseCase.Result.InvalidInput -> { updateLoginFlowState(NewLoginFlowState.Error.TextFieldError.InvalidValue) @@ -199,6 +165,43 @@ class NewLoginViewModel( } } + private suspend fun tryStartSharedAuthLogin(userIdentifier: String): Boolean = + sharedAuthNewLoginAdapter.tryStartLogin( + request = SharedAuthNewLoginRequest( + userIdentifier = userIdentifier, + serverConfig = serverConfig, + customServerConfig = loginNavArgs.loginPasswordPath?.customServerConfig, + ), + callbacks = object : SharedAuthNewLoginCallbacks { + override suspend fun showInvalidInput() { + updateLoginFlowState(NewLoginFlowState.Error.TextFieldError.InvalidValue) + } + + override suspend fun showGenericError(failure: CoreFailure) { + updateLoginFlowState(NewLoginFlowState.Error.DialogError.GenericError(failure)) + } + + override suspend fun showCustomServerDialog(serverLinks: ServerConfig.Links) { + updateLoginFlowState(NewLoginFlowState.CustomConfigDialog(serverLinks)) + } + + override suspend fun openEmailPassword(userIdentifier: String, loginPasswordPath: LoginPasswordPath) { + sendAction(NewLoginAction.EmailPassword(userIdentifier, loginPasswordPath)) + updateLoginFlowState(NewLoginFlowState.Default) + } + + override suspend fun openSso(url: String, config: SSOUrlConfig) { + sendAction(NewLoginAction.SSO(url, config)) + updateLoginFlowState(NewLoginFlowState.Default) + } + + override suspend fun openEnterpriseLoginNotSupported(userIdentifier: String) { + sendAction(NewLoginAction.EnterpriseLoginNotSupported(userIdentifier)) + updateLoginFlowState(NewLoginFlowState.Default) + } + } + ) + @VisibleForTesting internal suspend fun getEnterpriseLoginFlow(email: String) = withContext(dispatchers.io()) { ssoExtension.withAuthenticationScope( @@ -308,18 +311,6 @@ class NewLoginViewModel( ) } - fun observeSSOResult(backStackSavedState: SavedStateHandle) { - viewModelScope.launch { - backStackSavedState - .getStateFlow(SSO_LOGIN_RESULT_KEY, null) - .filterNotNull() - .collect { json -> - handleSSOResult(Json.decodeFromString(json)) - backStackSavedState.remove(SSO_LOGIN_RESULT_KEY) - } - } - } - fun handleSSOResult(ssoLoginResult: DeepLinkResult.SSOLogin) { updateLoginFlowState(NewLoginFlowState.Loading) when (ssoLoginResult) { @@ -404,15 +395,12 @@ class NewLoginViewModel( } catch (e: CancellationException) { throw e } catch (e: Exception) { - when (e) { - is IllegalStateException, is IOException, is SQLiteException -> { - if (isSessionStillValid(storedUserId)) throw e - appLogger.w("$TAG Crypto restore interrupted by concurrent logout: ${e.message}") - return - } - - else -> throw e + if (recoverableLogoutExceptionDetector.isRecoverableLogoutInterruption(e)) { + if (isSessionStillValid(storedUserId)) throw e + appLogger.w("$TAG Crypto restore interrupted by concurrent logout: ${e.message}") + return } + throw e } when (restoreResult) { @@ -452,14 +440,12 @@ class NewLoginViewModel( } catch (e: CancellationException) { throw e } catch (e: Exception) { - when (e) { - is IllegalStateException, is IOException, is SQLiteException -> { - if (isSessionStillValid(userId)) throw e - appLogger.w("$TAG Failed to revert SSO session, may have been already logged out: ${e.message}") - } - - else -> throw e + if (recoverableLogoutExceptionDetector.isRecoverableLogoutInterruption(e)) { + if (isSessionStillValid(userId)) throw e + appLogger.w("$TAG Failed to revert SSO session, may have been already logged out: ${e.message}") + return } + throw e } } diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelFactory.kt new file mode 100644 index 00000000000..32c114a0fb4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelFactory.kt @@ -0,0 +1,66 @@ +/* + * 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.newauthentication.login + +import com.wire.android.datastore.UserDataStoreProvider +import com.wire.android.di.ClientScopeProvider +import com.wire.android.di.DefaultWebSocketEnabledByDefault +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.ui.authentication.login.LoginNavArgs +import com.wire.android.ui.authentication.login.LoginSavedInputStore +import com.wire.android.ui.authentication.login.LoginViewModelExtension +import com.wire.android.ui.authentication.login.sso.LoginSSOViewModelExtension +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Named + +@Inject +@Suppress("LongParameterList") +class NewLoginViewModelFactory( + private val validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase, + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val addAuthenticatedUser: AddAuthenticatedUserUseCase, + private val clientScopeProviderFactory: ClientScopeProvider.Factory, + private val userDataStoreProvider: UserDataStoreProvider, + private val dispatchers: DispatcherProvider, + private val defaultServerConfig: ServerConfig.Links, + @Named("ssoCodeConfig") private val defaultSSOCodeConfig: String, + @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean, + private val recoverableLogoutExceptionDetector: NewLoginRecoverableLogoutExceptionDetector, +) { + fun create( + args: LoginNavArgs, + savedInputStore: LoginSavedInputStore, + ): NewLoginViewModel = NewLoginViewModel( + loginNavArgs = args, + validateEmailOrSSOCode = validateEmailOrSSOCode, + coreLogic = coreLogic, + savedInputStore = savedInputStore, + clientScopeProviderFactory = clientScopeProviderFactory, + userDataStoreProvider = userDataStoreProvider, + loginExtension = LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), + ssoExtension = LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic, defaultWebSocketEnabledByDefault), + dispatchers = dispatchers, + defaultServerConfig = defaultServerConfig, + defaultSSOCodeConfig = defaultSSOCodeConfig, + recoverableLogoutExceptionDetector = recoverableLogoutExceptionDetector, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/SharedAuthNewLoginAdapter.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/SharedAuthNewLoginAdapter.kt new file mode 100644 index 00000000000..f34c060c2e9 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/SharedAuthNewLoginAdapter.kt @@ -0,0 +1,59 @@ +/* + * 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.newauthentication.login + +import com.wire.android.ui.authentication.login.LoginPasswordPath +import com.wire.android.ui.authentication.login.sso.SSOUrlConfig +import com.wire.kalium.common.error.CoreFailure +import com.wire.kalium.logic.configuration.server.ServerConfig + +/** + * Android-side boundary for replacing the new login identifier step with shared/auth. + * + * The production shared/auth implementation should map shared state/effects into these Android callbacks. + * Android keeps Compose text fields, navigation, dialogs, custom tabs and resources in app. + */ +fun interface SharedAuthNewLoginAdapter { + suspend fun tryStartLogin( + request: SharedAuthNewLoginRequest, + callbacks: SharedAuthNewLoginCallbacks, + ): Boolean +} + +data class SharedAuthNewLoginRequest( + val userIdentifier: String, + val serverConfig: ServerConfig.Links, + val customServerConfig: ServerConfig.Links?, +) + +interface SharedAuthNewLoginCallbacks { + suspend fun showInvalidInput() + suspend fun showGenericError(failure: CoreFailure) + suspend fun showCustomServerDialog(serverLinks: ServerConfig.Links) + suspend fun openEmailPassword(userIdentifier: String, loginPasswordPath: LoginPasswordPath) + suspend fun openSso(url: String, config: SSOUrlConfig) + suspend fun openEnterpriseLoginNotSupported(userIdentifier: String) +} + +object LegacySharedAuthNewLoginAdapter : SharedAuthNewLoginAdapter { + override suspend fun tryStartLogin( + request: SharedAuthNewLoginRequest, + callbacks: SharedAuthNewLoginCallbacks, + ): Boolean = false +} diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/ValidateEmailOrSSOCodeUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/ValidateEmailOrSSOCodeUseCase.kt index 773b3372dc6..2497ab48881 100644 --- a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/ValidateEmailOrSSOCodeUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/ValidateEmailOrSSOCodeUseCase.kt @@ -20,13 +20,11 @@ package com.wire.android.ui.newauthentication.login import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase import com.wire.kalium.logic.feature.auth.sso.ValidateSSOCodeResult import com.wire.kalium.logic.feature.auth.sso.ValidateSSOCodeUseCase -import dagger.hilt.android.scopes.ViewModelScoped import javax.inject.Inject /** * Validates the input for a SSO code or an email address valid format. */ -@ViewModelScoped class ValidateEmailOrSSOCodeUseCase @Inject constructor( val validateEmail: ValidateEmailUseCase, val validateSSOCode: ValidateSSOCodeUseCase diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt index e30f391b07f..0e97d2246ec 100644 --- a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt @@ -46,10 +46,10 @@ import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.BuildConfig import com.wire.android.BuildConfig.ENABLE_NEW_REGISTRATION import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -105,7 +105,9 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun NewLoginPasswordScreen( navigator: Navigator, navArgs: LoginNavArgs, - loginEmailViewModel: LoginEmailViewModel = hiltViewModel() + loginEmailViewModel: LoginEmailViewModel = metroViewModel { + loginEmailViewModelFactory.create(navArgs) + } ) { clearAutofillTree() LoginStateNavigationAndDialogs(loginEmailViewModel, navigator) diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeScreen.kt index fabc3997e2f..77c7c081a6b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeScreen.kt @@ -41,8 +41,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -80,7 +80,10 @@ import kotlinx.coroutines.job @Composable fun CreateAccountVerificationCodeScreen( navigator: Navigator, - createAccountCodeVerificationViewModel: CreateAccountVerificationCodeViewModel = hiltViewModel() + args: CreateAccountDataNavArgs, + createAccountCodeVerificationViewModel: CreateAccountVerificationCodeViewModel = metroViewModel { + createAccountVerificationCodeViewModelFactory.create(args) + } ) { with(createAccountCodeVerificationViewModel) { fun navigateToUsernameScreen() = navigator.navigate( diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt index b95bd2e6720..04d9649f1a9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt @@ -22,15 +22,11 @@ import androidx.compose.foundation.text.input.clearText import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.BuildConfig import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase import com.wire.android.di.ClientScopeProvider -import com.wire.android.di.DefaultWebSocketEnabledByDefault -import com.wire.android.di.KaliumCoreLogic import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.ui.authentication.create.common.CreateAccountDataNavArgs import com.wire.android.ui.common.textfield.textAsFlow @@ -46,24 +42,19 @@ import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.register.RegisterParam import com.wire.kalium.logic.feature.register.RegisterResult import com.wire.kalium.logic.feature.register.RequestActivationCodeResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CreateAccountVerificationCodeViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - @KaliumCoreLogic private val coreLogic: CoreLogic, +class CreateAccountVerificationCodeViewModel( + val createAccountNavArgs: CreateAccountDataNavArgs, + private val coreLogic: CoreLogic, private val addAuthenticatedUser: AddAuthenticatedUserUseCase, private val registrationAnalyticsManager: RegistrationAnalyticsManagerUseCase, private val clientScopeProviderFactory: ClientScopeProvider.Factory, defaultServerConfig: ServerConfig.Links, - @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean, + private val defaultWebSocketEnabledByDefault: Boolean, ) : ViewModel() { - val createAccountNavArgs: CreateAccountDataNavArgs = savedStateHandle.navArgs() - val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig ?: defaultServerConfig val codeTextState: TextFieldState = TextFieldState() diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModelFactory.kt new file mode 100644 index 00000000000..1e92e2f2010 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModelFactory.kt @@ -0,0 +1,48 @@ +/* + * 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.registration.code + +import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase +import com.wire.android.di.ClientScopeProvider +import com.wire.android.di.DefaultWebSocketEnabledByDefault +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.ui.authentication.create.common.CreateAccountDataNavArgs +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +class CreateAccountVerificationCodeViewModelFactory( + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val addAuthenticatedUser: AddAuthenticatedUserUseCase, + private val registrationAnalyticsManager: RegistrationAnalyticsManagerUseCase, + private val clientScopeProviderFactory: ClientScopeProvider.Factory, + private val defaultServerConfig: ServerConfig.Links, + @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean, +) { + fun create(args: CreateAccountDataNavArgs): CreateAccountVerificationCodeViewModel = CreateAccountVerificationCodeViewModel( + createAccountNavArgs = args, + coreLogic = coreLogic, + addAuthenticatedUser = addAuthenticatedUser, + registrationAnalyticsManager = registrationAnalyticsManager, + clientScopeProviderFactory = clientScopeProviderFactory, + defaultServerConfig = defaultServerConfig, + defaultWebSocketEnabledByDefault = defaultWebSocketEnabledByDefault, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailScreen.kt b/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailScreen.kt index d9cfda364c0..5dcacb47ad4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailScreen.kt @@ -53,8 +53,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withLink import androidx.compose.ui.text.withStyle -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.AuthPopUpNavigationAnimation @@ -95,7 +95,10 @@ import com.wire.kalium.logic.configuration.server.ServerConfig @Composable fun CreateAccountDataDetailScreen( navigator: Navigator, - createAccountDataDetailViewModel: CreateAccountDataDetailViewModel = hiltViewModel() + args: CreateAccountDataNavArgs, + createAccountDataDetailViewModel: CreateAccountDataDetailViewModel = metroViewModel { + createAccountDataDetailViewModelFactory.create(args) + } ) { with(createAccountDataDetailViewModel) { fun navigateToCodeScreen() = navigator.navigate( diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModel.kt index c2f35a041bb..6b318e6bb1e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModel.kt @@ -21,42 +21,34 @@ import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase import com.wire.android.datastore.GlobalDataStore -import com.wire.android.di.KaliumCoreLogic import com.wire.android.feature.analytics.model.AnalyticsEvent.RegistrationPersonalAccount import com.wire.android.ui.authentication.create.common.CreateAccountDataNavArgs import com.wire.android.ui.common.textfield.textAsFlow -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase import com.wire.kalium.logic.feature.register.RequestActivationCodeResult -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds -@HiltViewModel -class CreateAccountDataDetailViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class CreateAccountDataDetailViewModel( + val createAccountNavArgs: CreateAccountDataNavArgs, private val validatePassword: ValidatePasswordUseCase, private val validateEmail: ValidateEmailUseCase, private val globalDataStore: GlobalDataStore, private val registrationAnalyticsManager: RegistrationAnalyticsManagerUseCase, - @KaliumCoreLogic private val coreLogic: CoreLogic, + private val coreLogic: CoreLogic, defaultServerConfig: ServerConfig.Links ) : ViewModel() { - val createAccountNavArgs: CreateAccountDataNavArgs = savedStateHandle.navArgs() - private var withPasswordTries = false val emailTextState: TextFieldState = TextFieldState(createAccountNavArgs.userRegistrationInfo.email) val nameTextState: TextFieldState = TextFieldState() diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelFactory.kt new file mode 100644 index 00000000000..ee7affa8a12 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelFactory.kt @@ -0,0 +1,48 @@ +/* + * 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.registration.details + +import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.ui.authentication.create.common.CreateAccountDataNavArgs +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase +import dev.zacsweers.metro.Inject + +@Inject +class CreateAccountDataDetailViewModelFactory( + private val validatePassword: ValidatePasswordUseCase, + private val validateEmail: ValidateEmailUseCase, + private val globalDataStore: GlobalDataStore, + private val registrationAnalyticsManager: RegistrationAnalyticsManagerUseCase, + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val defaultServerConfig: ServerConfig.Links, +) { + fun create(args: CreateAccountDataNavArgs): CreateAccountDataDetailViewModel = CreateAccountDataDetailViewModel( + createAccountNavArgs = args, + validatePassword = validatePassword, + validateEmail = validateEmail, + globalDataStore = globalDataStore, + registrationAnalyticsManager = registrationAnalyticsManager, + coreLogic = coreLogic, + defaultServerConfig = defaultServerConfig, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorScreen.kt b/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorScreen.kt index bc768a36a4e..bf23ca0151c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorScreen.kt @@ -46,8 +46,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.core.net.toUri -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -82,7 +82,8 @@ import com.wire.android.ui.common.R as commonR @Composable fun CreateAccountSelectorScreen( navigator: Navigator, - viewModel: CreateAccountSelectorViewModel = hiltViewModel() + args: CreateAccountSelectorNavArgs, + viewModel: CreateAccountSelectorViewModel = metroViewModel { createAccountSelectorViewModelFactory.create(args) } ) { val context = LocalContext.current fun navigateToEmailScreen() { diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModel.kt index f0b616a24b6..88a25747a4b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModel.kt @@ -17,23 +17,17 @@ */ package com.wire.android.ui.registration.selector -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.datastore.GlobalDataStore -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.configuration.server.ServerConfig -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CreateAccountSelectorViewModel @Inject constructor( +class CreateAccountSelectorViewModel( + navArgs: CreateAccountSelectorNavArgs, private val globalDataStore: GlobalDataStore, - savedStateHandle: SavedStateHandle, defaultServerConfig: ServerConfig.Links ) : ViewModel() { - val navArgs: CreateAccountSelectorNavArgs = savedStateHandle.navArgs() val serverConfig: ServerConfig.Links = navArgs.customServerConfig ?: defaultServerConfig val email: String = navArgs.email.orEmpty() val teamAccountCreationUrl = serverConfig.teams diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelFactory.kt new file mode 100644 index 00000000000..90d521333af --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelFactory.kt @@ -0,0 +1,34 @@ +/* + * 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.registration.selector + +import com.wire.android.datastore.GlobalDataStore +import com.wire.kalium.logic.configuration.server.ServerConfig +import dev.zacsweers.metro.Inject + +@Inject +class CreateAccountSelectorViewModelFactory( + private val globalDataStore: GlobalDataStore, + private val defaultServerConfig: ServerConfig.Links, +) { + fun create(args: CreateAccountSelectorNavArgs): CreateAccountSelectorViewModel = CreateAccountSelectorViewModel( + navArgs = args, + globalDataStore = globalDataStore, + defaultServerConfig = defaultServerConfig, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppInfoProvider.kt b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppInfoProvider.kt new file mode 100644 index 00000000000..d818a033662 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppInfoProvider.kt @@ -0,0 +1,36 @@ +/* + * 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.settings.about + +import android.content.Context +import com.wire.android.util.AppNameUtil +import com.wire.android.util.getGitBuildId + +interface AboutThisAppInfoProvider { + val appName: String + fun gitBuildId(): String +} + +class AndroidAboutThisAppInfoProvider( + private val context: Context +) : AboutThisAppInfoProvider { + override val appName: String + get() = AppNameUtil.createAppName() + + override fun gitBuildId(): String = context.getGitBuildId() +} diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppScreen.kt index 8c3abd8d5e7..6343a5af20c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppScreen.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.Clickable import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -52,7 +52,9 @@ import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun AboutThisAppScreen( navigator: Navigator, - viewModel: AboutThisAppViewModel = hiltViewModel() + viewModel: AboutThisAppViewModel = metroViewModel { + aboutThisAppViewModelFactory.create() + } ) { val context = LocalContext.current AboutThisAppContent( diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModel.kt index 523c2e69765..08d8a050b14 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModel.kt @@ -17,27 +17,20 @@ */ package com.wire.android.ui.settings.about -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.util.AppNameUtil -import com.wire.android.util.getGitBuildId -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class AboutThisAppViewModel @Inject constructor( - @ApplicationContext private val context: Context +class AboutThisAppViewModel( + private val aboutThisAppInfoProvider: AboutThisAppInfoProvider ) : ViewModel() { var state by mutableStateOf( AboutThisAppState( - appName = AppNameUtil.createAppName() + appName = aboutThisAppInfoProvider.appName ) ) @@ -47,7 +40,7 @@ class AboutThisAppViewModel @Inject constructor( private fun setGitHash() { viewModelScope.launch { - val gitBuildId = context.getGitBuildId() + val gitBuildId = aboutThisAppInfoProvider.gitBuildId() state = state.copy( commitish = gitBuildId ) diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModelFactory.kt new file mode 100644 index 00000000000..26770e7ad16 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModelFactory.kt @@ -0,0 +1,29 @@ +/* + * 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.settings.about + +import dev.zacsweers.metro.Inject + +@Inject +class AboutThisAppViewModelFactory( + private val aboutThisAppInfoProvider: AboutThisAppInfoProvider, +) { + fun create(): AboutThisAppViewModel = AboutThisAppViewModel( + aboutThisAppInfoProvider = aboutThisAppInfoProvider, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index 201241f452e..d01db71074b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt @@ -45,9 +45,9 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.BuildConfig import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.SlideNavigationAnimation @@ -107,7 +107,8 @@ import kotlinx.datetime.Instant @Composable fun DeviceDetailsScreen( navigator: Navigator, - viewModel: DeviceDetailsViewModel = hiltViewModel() + args: DeviceDetailsNavArgs, + viewModel: DeviceDetailsViewModel = metroViewModel { deviceDetailsViewModelFactory.create(args) } ) { when { viewModel.state.error is RemoveDeviceError.InitError -> navigator.navigateBack() diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt index a2446f7ec16..f8797207d80 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.text.input.clearText import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger @@ -31,7 +30,6 @@ import com.wire.android.ui.authentication.devices.model.Device import com.wire.android.ui.authentication.devices.remove.RemoveDeviceDialogState import com.wire.android.ui.authentication.devices.remove.RemoveDeviceError import com.wire.android.ui.common.textfield.textAsFlow -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.ui.settings.devices.model.DeviceDetailsState import com.wire.kalium.logic.data.client.ClientType import com.wire.kalium.logic.data.client.DeleteClientParam @@ -53,16 +51,13 @@ import com.wire.kalium.logic.feature.user.GetUserInfoResult import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel -class DeviceDetailsViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class DeviceDetailsViewModel( + private val deviceDetailsNavArgs: DeviceDetailsNavArgs, @CurrentAccount private val currentUserId: UserId, private val deleteClient: DeleteClientUseCase, @@ -76,7 +71,6 @@ class DeviceDetailsViewModel @Inject constructor( private val isE2EIEnabledUseCase: IsE2EIEnabledUseCase ) : ViewModel() { - private val deviceDetailsNavArgs: DeviceDetailsNavArgs = savedStateHandle.navArgs() private val deviceId: ClientId = deviceDetailsNavArgs.clientId private val userId: UserId = deviceDetailsNavArgs.userId diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelFactory.kt new file mode 100644 index 00000000000..14195c5d7dc --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelFactory.kt @@ -0,0 +1,60 @@ +/* + * 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.settings.devices + +import com.wire.android.di.CurrentAccount +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.client.ClientFingerprintUseCase +import com.wire.kalium.logic.feature.client.DeleteClientUseCase +import com.wire.kalium.logic.feature.client.ObserveClientDetailsUseCase +import com.wire.kalium.logic.feature.client.UpdateClientVerificationStatusUseCase +import com.wire.kalium.logic.feature.debug.BreakSessionUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.GetMLSClientIdentityUseCase +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase +import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase +import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class DeviceDetailsViewModelFactory( + @CurrentAccount private val currentUserId: UserId, + private val deleteClient: DeleteClientUseCase, + private val observeClientDetails: ObserveClientDetailsUseCase, + private val isPasswordRequired: IsPasswordRequiredUseCase, + private val fingerprintUseCase: ClientFingerprintUseCase, + private val updateClientVerificationStatus: UpdateClientVerificationStatusUseCase, + private val observeUserInfo: ObserveUserInfoUseCase, + private val mlsClientIdentity: GetMLSClientIdentityUseCase, + private val breakSession: BreakSessionUseCase, + private val isE2EIEnabledUseCase: IsE2EIEnabledUseCase, +) { + fun create(args: DeviceDetailsNavArgs): DeviceDetailsViewModel = DeviceDetailsViewModel( + deviceDetailsNavArgs = args, + currentUserId = currentUserId, + deleteClient = deleteClient, + observeClientDetails = observeClientDetails, + isPasswordRequired = isPasswordRequired, + fingerprintUseCase = fingerprintUseCase, + updateClientVerificationStatus = updateClientVerificationStatus, + observeUserInfo = observeUserInfo, + mlsClientIdentity = mlsClientIdentity, + breakSession = breakSession, + isE2EIEnabledUseCase = isE2EIEnabledUseCase, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt index dfd475ef1ca..03b349739a6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesScreen.kt @@ -33,9 +33,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.authentication.devices.DeviceItem @@ -56,7 +56,9 @@ import com.wire.kalium.logic.data.conversation.ClientId @Composable fun SelfDevicesScreen( navigator: Navigator, - viewModel: SelfDevicesViewModel = hiltViewModel() + viewModel: SelfDevicesViewModel = metroViewModel { + selfDevicesViewModelFactory.create() + } ) { val lifecycleEvent = rememberLifecycleEvent() LaunchedEffect(lifecycleEvent) { diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt index cd212fcdccc..4cc09688cd1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt @@ -32,16 +32,13 @@ import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserMlsClientIdentitiesUseCase import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class SelfDevicesViewModel @Inject constructor( +class SelfDevicesViewModel( @CurrentAccount val currentAccountId: UserId, private val fetchSelfClientsFromRemote: FetchSelfClientsFromRemoteUseCase, private val observeClientList: ObserveClientsByUserIdUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModelFactory.kt new file mode 100644 index 00000000000..d79845cc36a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModelFactory.kt @@ -0,0 +1,46 @@ +/* + * 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.settings.devices + +import com.wire.android.di.CurrentAccount +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.client.FetchSelfClientsFromRemoteUseCase +import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase +import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.GetUserMlsClientIdentitiesUseCase +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase +import dev.zacsweers.metro.Inject + +@Inject +class SelfDevicesViewModelFactory( + @CurrentAccount private val currentAccountId: UserId, + private val fetchSelfClientsFromRemote: FetchSelfClientsFromRemoteUseCase, + private val observeClientList: ObserveClientsByUserIdUseCase, + private val currentClientIdUseCase: ObserveCurrentClientIdUseCase, + private val getUserMlsClientIdentities: GetUserMlsClientIdentitiesUseCase, + private val isE2EIEnabledUseCase: IsE2EIEnabledUseCase, +) { + fun create(): SelfDevicesViewModel = SelfDevicesViewModel( + currentAccountId = currentAccountId, + fetchSelfClientsFromRemote = fetchSelfClientsFromRemote, + observeClientList = observeClientList, + currentClientIdUseCase = currentClientIdUseCase, + getUserMlsClientIdentities = getUserMlsClientIdentities, + isE2EIEnabledUseCase = isE2EIEnabledUseCase, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt index e73991499ee..e03d1dd6f83 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.settings.devices.e2ei -import com.wire.android.navigation.annotation.app.WireRootDestination import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding @@ -34,7 +33,8 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.di.metro.metroViewModel +import com.wire.android.navigation.annotation.app.WireRootDestination import com.wire.android.R import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.PopUpNavigationAnimation @@ -61,7 +61,10 @@ import kotlinx.coroutines.withContext @Composable fun E2eiCertificateDetailsScreen( navigator: Navigator, - e2eiCertificateDetailsViewModel: E2eiCertificateDetailsViewModel = hiltViewModel() + args: E2eiCertificateDetailsScreenNavArgs, + e2eiCertificateDetailsViewModel: E2eiCertificateDetailsViewModel = metroViewModel { + e2eiCertificateDetailsViewModelFactory.create(args) + } ) { val snackbarHostState = LocalSnackbarHostState.current val scope = rememberCoroutineScope() diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt index b4f3f5bec34..db0b6024225 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt @@ -17,24 +17,17 @@ */ package com.wire.android.ui.settings.devices.e2ei -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.fileDateTime import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.util.DateTimeUtil -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class E2eiCertificateDetailsViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +class E2eiCertificateDetailsViewModel( + private val navArgs: E2eiCertificateDetailsScreenNavArgs, private val getSelfUser: GetSelfUserUseCase, ) : ViewModel() { - private val navArgs: E2eiCertificateDetailsScreenNavArgs = - savedStateHandle.navArgs() private var selfUserHandle: String? = null diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModelFactory.kt new file mode 100644 index 00000000000..b3dfaa5fb4a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModelFactory.kt @@ -0,0 +1,32 @@ +/* + * 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.settings.devices.e2ei + +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +class E2eiCertificateDetailsViewModelFactory( + private val getSelfUser: GetSelfUserUseCase, +) { + fun create(args: E2eiCertificateDetailsScreenNavArgs): E2eiCertificateDetailsViewModel = + E2eiCertificateDetailsViewModel( + navArgs = args, + getSelfUser = getSelfUser, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAssetImporter.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAssetImporter.kt new file mode 100644 index 00000000000..3384dacff2f --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAssetImporter.kt @@ -0,0 +1,54 @@ +/* + * 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.sharing + +import androidx.core.net.toUri +import com.wire.android.appLogger +import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import kotlinx.coroutines.withContext + +interface ImportMediaAssetImporter { + suspend fun importAsset(uri: String): ImportedMediaAsset? +} + +class ImportMediaAssetImporterImpl( + private val handleUriAsset: HandleUriAssetUseCase, + private val dispatchers: DispatcherProvider, +) : ImportMediaAssetImporter { + + override suspend fun importAsset(uri: String): ImportedMediaAsset? = withContext(dispatchers.io()) { + when (val result = handleUriAsset.invoke(uri.toUri(), saveToDeviceIfInvalid = false)) { + is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> { + appLogger.w("$TAG: Failed to import asset message: Asset too large") + ImportedMediaAsset(result.assetBundle, result.maxLimitInMB) + } + + HandleUriAssetUseCase.Result.Failure.Unknown -> { + appLogger.e("$TAG: Failed to import asset message: Unknown error") + null + } + + is HandleUriAssetUseCase.Result.Success -> ImportedMediaAsset(result.assetBundle, null) + } + } + + private companion object { + const val TAG = "[ImportMediaAssetImporter]" + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt index b3277ca18cd..1d667e86384 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt @@ -17,16 +17,10 @@ */ package com.wire.android.ui.sharing -import android.content.Intent -import android.net.Uri -import android.os.Parcelable -import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.app.ShareCompat -import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData @@ -38,19 +32,16 @@ import com.wire.android.model.SnackBarMessage import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.ui.common.DEFAULT_SEARCH_QUERY_DEBOUNCE import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase -import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase import com.wire.android.ui.home.conversationslist.model.ConversationItemType import com.wire.android.ui.home.conversationslist.model.ConversationItem import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration import com.wire.android.util.EMPTY import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.parcelableArrayList import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.data.message.SelfDeletionTimer.Companion.SELF_DELETION_LOG_TAG import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.FlowPreview @@ -65,16 +56,13 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel @OptIn(FlowPreview::class) @Suppress("LongParameterList", "TooManyFunctions") -class ImportMediaAuthenticatedViewModel @Inject constructor( +class ImportMediaAuthenticatedViewModel( private val getSelf: ObserveSelfUserUseCase, private val getConversationsPaginated: GetConversationsFromSearchUseCase, - private val handleUriAsset: HandleUriAssetUseCase, + private val importMediaAssetImporter: ImportMediaAssetImporter, private val persistNewSelfDeletionTimerUseCase: PersistNewSelfDeletionTimerUseCase, private val observeSelfDeletionSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase, val dispatchers: DispatcherProvider, @@ -147,19 +135,18 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( } } - suspend fun handleReceivedDataFromSharingIntent(activity: AppCompatActivity) { - val incomingIntent = ShareCompat.IntentReader(activity) - appLogger.i("Received data from sharing intent ${incomingIntent.streamCount}") + suspend fun handleReceivedDataFromSharingIntent(sharedContent: ImportMediaSharingContent) { + appLogger.i("Received data from sharing intent ${sharedContent.streamCount}") importMediaState = importMediaState.copy(isImporting = true) - if (incomingIntent.streamCount == 0) { - handleSharedText(incomingIntent.text.toString()) + if (!sharedContent.hasStreams) { + handleSharedText(sharedContent.text.toString()) } else { - if (incomingIntent.isSingleShare) { + if (sharedContent.isSingleShare) { // ACTION_SEND - handleSingleIntent(incomingIntent) + sharedContent.assetUris.firstOrNull()?.let { handleSingleIntent(it) } } else { // ACTION_SEND_MULTIPLE - handleMultipleActionIntent(activity) + handleMultipleActionIntent(sharedContent.assetUris) } } importMediaState = importMediaState.copy(isImporting = false) @@ -170,26 +157,21 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( importMediaState = importMediaState.copy(importedText = text) } - private suspend fun handleSingleIntent(incomingIntent: ShareCompat.IntentReader) { - incomingIntent.stream?.let { uri -> - appLogger.d("$TAG: handleSingleIntent") - handleImportedAsset(uri)?.let { importedAsset -> - if (importedAsset.assetSizeExceeded != null) { - onSnackbarMessage( - SendMessagesSnackbarMessages.MaxAssetSizeExceeded(importedAsset.assetSizeExceeded) - ) - } - importMediaState = importMediaState.copy(importedAssets = persistentListOf(importedAsset)) + private suspend fun handleSingleIntent(uri: String) { + appLogger.d("$TAG: handleSingleIntent") + handleImportedAsset(uri)?.let { importedAsset -> + if (importedAsset.assetSizeExceeded != null) { + onSnackbarMessage( + SendMessagesSnackbarMessages.MaxAssetSizeExceeded(importedAsset.assetSizeExceeded) + ) } + importMediaState = importMediaState.copy(importedAssets = persistentListOf(importedAsset)) } } - private suspend fun handleMultipleActionIntent(activity: AppCompatActivity) { + private suspend fun handleMultipleActionIntent(assetUris: List) { appLogger.d("$TAG: handleMultipleActionIntent") - val importedMediaAssets = activity.intent.parcelableArrayList(Intent.EXTRA_STREAM)?.mapNotNull { - val fileUri = it.toString().toUri() - handleImportedAsset(fileUri) - } ?: listOf() + val importedMediaAssets = assetUris.mapNotNull { handleImportedAsset(it) } importMediaState = importMediaState.copy(importedAssets = importedMediaAssets.toPersistentList()) @@ -215,21 +197,7 @@ class ImportMediaAuthenticatedViewModel @Inject constructor( } } - private suspend fun handleImportedAsset(uri: Uri): ImportedMediaAsset? = withContext(dispatchers.io()) { - when (val result = handleUriAsset.invoke(uri, saveToDeviceIfInvalid = false)) { - is HandleUriAssetUseCase.Result.Failure.AssetTooLarge -> { - appLogger.w("$TAG: Failed to import asset message: Asset too large") - ImportedMediaAsset(result.assetBundle, result.maxLimitInMB) - } - - HandleUriAssetUseCase.Result.Failure.Unknown -> { - appLogger.e("$TAG: Failed to import asset message: Unknown error") - null - } - - is HandleUriAssetUseCase.Result.Success -> ImportedMediaAsset(result.assetBundle, null) - } - } + private suspend fun handleImportedAsset(uri: String): ImportedMediaAsset? = importMediaAssetImporter.importAsset(uri) private fun onSnackbarMessage(type: SnackBarMessage) = viewModelScope.launch { _infoMessage.emit(type) diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelFactory.kt new file mode 100644 index 00000000000..814ea7a7264 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelFactory.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.sharing + +import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase +import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +class ImportMediaAuthenticatedViewModelFactory( + private val getSelf: ObserveSelfUserUseCase, + private val getConversationsPaginated: GetConversationsFromSearchUseCase, + private val importMediaAssetImporter: ImportMediaAssetImporter, + private val persistNewSelfDeletionTimerUseCase: PersistNewSelfDeletionTimerUseCase, + private val observeSelfDeletionSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase, + private val dispatchers: DispatcherProvider, +) { + fun create(): ImportMediaAuthenticatedViewModel = ImportMediaAuthenticatedViewModel( + getSelf = getSelf, + getConversationsPaginated = getConversationsPaginated, + importMediaAssetImporter = importMediaAssetImporter, + persistNewSelfDeletionTimerUseCase = persistNewSelfDeletionTimerUseCase, + observeSelfDeletionSettingsForConversation = observeSelfDeletionSettingsForConversation, + dispatchers = dispatchers, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt index d6c3e9511a1..93561539fb0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt @@ -55,12 +55,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.NewLoginScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.WelcomeScreenDestination import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset import com.wire.android.model.SnackBarMessage @@ -124,7 +124,9 @@ import okio.Path.Companion.toPath fun ImportMediaScreen( navigator: Navigator, loginTypeSelector: LoginTypeSelector, - featureFlagNotificationViewModel: FeatureFlagNotificationViewModel = hiltViewModel(), + featureFlagNotificationViewModel: FeatureFlagNotificationViewModel = metroViewModel { + featureFlagNotificationViewModelFactory.create() + }, ) { when (val fileSharingRestrictedState = featureFlagNotificationViewModel.featureFlagState.isFileSharingState) { FeatureFlagState.FileSharingState.Loading -> { @@ -192,8 +194,12 @@ private fun ImportMediaLoadingContent(navigateBack: () -> Unit) { private fun ImportMediaAuthenticatedContent( navigator: Navigator, isRestrictedInTeam: Boolean, - checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = hiltViewModel(), - importMediaViewModel: ImportMediaAuthenticatedViewModel = hiltViewModel(), + checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = metroViewModel { + checkAssetRestrictionsViewModelFactory.create() + }, + importMediaViewModel: ImportMediaAuthenticatedViewModel = metroViewModel { + importMediaAuthenticatedViewModelFactory.create() + }, ) { if (isRestrictedInTeam) { ImportMediaRestrictedContent( @@ -247,7 +253,9 @@ private fun ImportMediaAuthenticatedContent( LaunchedEffect(isImportingData()) { if (importedAssets.isEmpty() || importedText.isNullOrEmpty()) { context.getActivity() - ?.let { activity -> importMediaViewModel.handleReceivedDataFromSharingIntent(activity) } + ?.let { activity -> + importMediaViewModel.handleReceivedDataFromSharingIntent(activity.toImportMediaSharingContent()) + } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingContent.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingContent.kt new file mode 100644 index 00000000000..41b6a9bb186 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingContent.kt @@ -0,0 +1,27 @@ +/* + * 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.sharing + +data class ImportMediaSharingContent( + val text: CharSequence?, + val assetUris: List, + val streamCount: Int = assetUris.size, + val isSingleShare: Boolean = streamCount == 1, +) { + val hasStreams: Boolean get() = streamCount > 0 +} diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingIntentReader.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingIntentReader.kt new file mode 100644 index 00000000000..3366a05bbe1 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingIntentReader.kt @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.sharing + +import android.content.Intent +import android.os.Parcelable +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ShareCompat +import com.wire.android.util.parcelableArrayList + +fun AppCompatActivity.toImportMediaSharingContent(): ImportMediaSharingContent { + val incomingIntent = ShareCompat.IntentReader(this) + val assetUris = when { + incomingIntent.streamCount == 0 -> emptyList() + incomingIntent.isSingleShare -> listOfNotNull(incomingIntent.stream?.toString()) + else -> intent.parcelableArrayList(Intent.EXTRA_STREAM) + ?.map { it.toString() } + .orEmpty() + } + return ImportMediaSharingContent( + text = incomingIntent.text, + assetUris = assetUris, + streamCount = incomingIntent.streamCount, + isSingleShare = incomingIntent.isSingleShare, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingModule.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingModule.kt new file mode 100644 index 00000000000..d30907eced2 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingModule.kt @@ -0,0 +1,20 @@ +/* + * 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.sharing + +// Import media sharing bindings are provided by the Metro graph. diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarImageGateway.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarImageGateway.kt new file mode 100644 index 00000000000..9872c0c32bf --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarImageGateway.kt @@ -0,0 +1,64 @@ +/* + * 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.userprofile.avatarpicker + +import android.content.Context +import androidx.core.net.toUri +import com.wire.android.util.AvatarImageManager +import com.wire.android.util.ImageUtil +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.android.util.resampleImageAndCopyToTempPath +import com.wire.android.util.toByteArray +import okio.Path + +interface AvatarImageGateway { + fun getWritableAvatarUri(avatarPath: Path): String + fun getShareableTempAvatarUri(avatarPath: Path): String + suspend fun sanitizeAvatarImage(originalAvatarUri: String, avatarPath: Path) + suspend fun getAvatarImageSize(avatarUri: String): Long +} + +class AndroidAvatarImageGateway( + private val avatarImageManager: AvatarImageManager, + private val dispatchers: DispatcherProvider, + private val appContext: Context +) : AvatarImageGateway { + + override fun getWritableAvatarUri(avatarPath: Path): String = + avatarImageManager.getWritableAvatarUri(avatarPath).toString() + + override fun getShareableTempAvatarUri(avatarPath: Path): String = + avatarImageManager.getShareableTempAvatarUri(avatarPath).toString() + + /** + * Resamples the image and removes unnecessary metadata before uploading it. + * This avoids uploading unnecessarily large profile pictures and sensitive metadata. + */ + override suspend fun sanitizeAvatarImage(originalAvatarUri: String, avatarPath: Path) { + originalAvatarUri.toUri().resampleImageAndCopyToTempPath( + context = appContext, + tempCachePath = avatarPath, + sizeClass = ImageUtil.ImageSizeClass.Small, + shouldRemoveMetadata = true + ) + } + + override suspend fun getAvatarImageSize(avatarUri: String): Long = + avatarUri.toUri().toByteArray(appContext, dispatchers).size.toLong() +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPicker.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPicker.kt index 3719cdcb887..7c398a7ee62 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPicker.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPicker.kt @@ -35,9 +35,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.core.net.toUri -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.Navigator import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.ui.common.ArrowRightIcon @@ -72,7 +72,9 @@ import com.wire.android.util.ui.PreviewMultipleThemesForSquare fun AvatarPickerScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, - viewModel: AvatarPickerViewModel = hiltViewModel() + viewModel: AvatarPickerViewModel = metroViewModel { + avatarPickerViewModelFactory.create() + } ) { val permissionPermanentlyDeniedDialogState = rememberVisibilityState() @@ -82,12 +84,12 @@ fun AvatarPickerScreen( val state = rememberAvatarPickerState( onImageSelected = { originalUri -> - viewModel.updatePickedAvatarUri(originalUri, targetAvatarPath.toFile().toUri()) + viewModel.updatePickedAvatarUri(originalUri.toString(), targetAvatarPath.toFile().toUri().toString()) }, onPictureTaken = { - viewModel.updatePickedAvatarUri(targetAvatarUri, targetAvatarPath.toFile().toUri()) + viewModel.updatePickedAvatarUri(targetAvatarUri, targetAvatarPath.toFile().toUri().toString()) }, - targetPictureFileUri = targetAvatarUri, + targetPictureFileUri = targetAvatarUri.toUri(), onGalleryPermissionPermanentlyDenied = { permissionPermanentlyDeniedDialogState.show( PermissionPermanentlyDeniedDialogState.Visible( @@ -207,7 +209,7 @@ private fun AvatarPickerContent( @Composable fun AvatarPreview(pictureState: PictureState) { BulletHoleImagePreview( - imageUri = pictureState.avatarUri, + imageUri = pictureState.avatarUri.toUri(), contentDescription = stringResource(R.string.content_description_avatar_preview) ) } @@ -273,7 +275,7 @@ private fun AvatarPickerTopBar(onCloseClick: () -> Unit) { @Composable fun AvatarPickerPreview() = WireTheme { AvatarPickerContent( - pictureState = PictureState.Picked("https://example.com/avatar.jpg".toUri()), + pictureState = PictureState.Picked("https://example.com/avatar.jpg"), state = rememberAvatarPickerState( onImageSelected = {}, onCameraPermissionPermanentlyDenied = {}, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt index ca74144df89..5bdcf34d678 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt @@ -18,24 +18,16 @@ package com.wire.android.ui.userprofile.avatarpicker -import android.content.Context -import android.net.Uri import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.appLogger import com.wire.android.datastore.UserDataStore import com.wire.android.model.SnackBarMessage -import com.wire.android.util.AvatarImageManager -import com.wire.android.util.ImageUtil -import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.resampleImageAndCopyToTempPath -import com.wire.android.util.toByteArray import com.wire.android.util.ui.UIText import com.wire.kalium.common.error.NetworkFailure import com.wire.kalium.logic.data.asset.KaliumFileSystem @@ -44,27 +36,21 @@ import com.wire.kalium.logic.feature.asset.GetAvatarAssetUseCase import com.wire.kalium.logic.feature.asset.PublicAssetResult import com.wire.kalium.logic.feature.user.UploadAvatarResult import com.wire.kalium.logic.feature.user.UploadUserAvatarUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import okio.Path import java.io.FileNotFoundException -import javax.inject.Inject -@HiltViewModel @Suppress("LongParameterList") -class AvatarPickerViewModel @Inject constructor( +class AvatarPickerViewModel( private val dataStore: UserDataStore, private val getAvatarAsset: GetAvatarAssetUseCase, private val uploadUserAvatar: UploadUserAvatarUseCase, - private val avatarImageManager: AvatarImageManager, - private val dispatchers: DispatcherProvider, + private val avatarImageGateway: AvatarImageGateway, private val kaliumFileSystem: KaliumFileSystem, - private val qualifiedIdMapper: QualifiedIdMapper, - @ApplicationContext private val appContext: Context + private val qualifiedIdMapper: QualifiedIdMapper ) : ViewModel() { var pictureState by mutableStateOf(PictureState.Empty) @@ -78,7 +64,7 @@ class AvatarPickerViewModel @Inject constructor( val defaultAvatarPath: Path get() = kaliumFileSystem.selfUserAvatarPath() - val temporaryAvatarUri: Uri = avatarImageManager.getShareableTempAvatarUri(defaultAvatarPath) + val temporaryAvatarUri: String = avatarImageGateway.getShareableTempAvatarUri(defaultAvatarPath) init { loadInitialAvatarState() @@ -92,7 +78,7 @@ class AvatarPickerViewModel @Inject constructor( dataStore.avatarAssetId.first()?.apply { val qualifiedAsset = qualifiedIdMapper.fromStringToQualifiedID(this) val avatarRawPath = (getAvatarAsset(assetKey = qualifiedAsset) as PublicAssetResult.Success).assetPath - val currentAvatarUri = avatarImageManager.getWritableAvatarUri(avatarRawPath) + val currentAvatarUri = avatarImageGateway.getWritableAvatarUri(avatarRawPath) initialPictureLoadingState = InitialPictureLoadingState.Loaded(currentAvatarUri) pictureState = PictureState.Initial(currentAvatarUri) } ?: run { @@ -106,24 +92,11 @@ class AvatarPickerViewModel @Inject constructor( } } - fun updatePickedAvatarUri(originalUri: Uri, updatedUri: Uri) = viewModelScope.launch { - sanitizeAvatarImage(originalUri, defaultAvatarPath) + fun updatePickedAvatarUri(originalUri: String, updatedUri: String) = viewModelScope.launch { + avatarImageGateway.sanitizeAvatarImage(originalUri, defaultAvatarPath) pictureState = PictureState.Picked(updatedUri) } - /** - * Resamples the image and removes unnecessary metadata before uploading it. - * This to avoid uploading unnecessarily large images for profile pictures and sensitive metadata. - */ - private suspend fun sanitizeAvatarImage(originalAvatarUri: Uri, avatarPath: Path) { - originalAvatarUri.resampleImageAndCopyToTempPath( - context = appContext, - tempCachePath = avatarPath, - sizeClass = ImageUtil.ImageSizeClass.Small, - shouldRemoveMetadata = true - ) - } - fun uploadNewPickedAvatar() { val imgUri = pictureState.avatarUri @@ -132,7 +105,7 @@ class AvatarPickerViewModel @Inject constructor( val avatarPath = defaultAvatarPath try { - val imageDataSize = imgUri.toByteArray(appContext, dispatchers).size.toLong() + val imageDataSize = avatarImageGateway.getAvatarImageSize(imgUri) when (val result = uploadUserAvatar(avatarPath, imageDataSize)) { is UploadAvatarResult.Success -> { @@ -168,16 +141,16 @@ class AvatarPickerViewModel @Inject constructor( private sealed class InitialPictureLoadingState { data object None : InitialPictureLoadingState() data object Loading : InitialPictureLoadingState() - data class Loaded(val avatarUri: Uri) : InitialPictureLoadingState() + data class Loaded(val avatarUri: String) : InitialPictureLoadingState() } @Stable - sealed class PictureState(open val avatarUri: Uri) { - data class Uploading(override val avatarUri: Uri) : PictureState(avatarUri) - data class Initial(override val avatarUri: Uri) : PictureState(avatarUri) - data class Picked(override val avatarUri: Uri) : PictureState(avatarUri) - data class Completed(override val avatarUri: Uri, val assetId: String?) : PictureState(avatarUri) - data object Empty : PictureState("".toUri()) + sealed class PictureState(open val avatarUri: String) { + data class Uploading(override val avatarUri: String) : PictureState(avatarUri) + data class Initial(override val avatarUri: String) : PictureState(avatarUri) + data class Picked(override val avatarUri: String) : PictureState(avatarUri) + data class Completed(override val avatarUri: String, val assetId: String?) : PictureState(avatarUri) + data object Empty : PictureState("") } sealed class InfoMessageType(override val uiText: UIText) : SnackBarMessage { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModelFactory.kt new file mode 100644 index 00000000000..d32157c06b5 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModelFactory.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.userprofile.avatarpicker + +import com.wire.android.datastore.UserDataStore +import com.wire.kalium.logic.data.asset.KaliumFileSystem +import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.feature.asset.GetAvatarAssetUseCase +import com.wire.kalium.logic.feature.user.UploadUserAvatarUseCase +import dev.zacsweers.metro.Inject + +@Inject +class AvatarPickerViewModelFactory( + private val dataStore: UserDataStore, + private val getAvatarAsset: GetAvatarAssetUseCase, + private val uploadUserAvatar: UploadUserAvatarUseCase, + private val avatarImageGateway: AvatarImageGateway, + private val kaliumFileSystem: KaliumFileSystem, + private val qualifiedIdMapper: QualifiedIdMapper, +) { + fun create(): AvatarPickerViewModel = AvatarPickerViewModel( + dataStore = dataStore, + getAvatarAsset = getAvatarAsset, + uploadUserAvatar = uploadUserAvatar, + avatarImageGateway = avatarImageGateway, + kaliumFileSystem = kaliumFileSystem, + qualifiedIdMapper = qualifiedIdMapper, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index 6575bfd5566..891bc23bcc1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt @@ -49,11 +49,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.result.ResultRecipient import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -121,7 +121,9 @@ fun OtherUserProfileScreen( resultNavigator: ResultBackNavigator, conversationFoldersScreenResultRecipient: ResultRecipient, - viewModel: OtherUserProfileScreenViewModel = hiltViewModel() + viewModel: OtherUserProfileScreenViewModel = metroViewModel { + otherUserProfileScreenViewModelFactory.create(navArgs) + } ) { val snackbarHostState = LocalSnackbarHostState.current val context = LocalContext.current diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt index 43661e9528c..5cd842b128d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt @@ -21,7 +21,6 @@ package com.wire.android.ui.userprofile.other import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.mapper.UserTypeMapper @@ -32,7 +31,6 @@ import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.common.visbility.VisibilityState import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveConversationRoleForUserUseCase import com.wire.android.ui.home.conversationslist.model.BlockState -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.ui.userprofile.common.UsernameMapper.fromOtherUser import com.wire.android.ui.userprofile.group.RemoveConversationMemberState import com.wire.android.ui.userprofile.other.OtherUserProfileInfoMessageType.ChangeGroupRoleError @@ -53,7 +51,6 @@ import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase import com.wire.kalium.logic.feature.user.GetUserInfoResult import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow @@ -63,11 +60,10 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class OtherUserProfileScreenViewModel @Inject constructor( +class OtherUserProfileScreenViewModel( + private val otherUserProfileNavArgs: OtherUserProfileNavArgs, private val dispatchers: DispatcherProvider, private val observeUserInfo: ObserveUserInfoUseCase, private val userTypeMapper: UserTypeMapper, @@ -80,10 +76,8 @@ class OtherUserProfileScreenViewModel @Inject constructor( private val isOneToOneConversationCreated: IsOneToOneConversationCreatedUseCase, private val mlsClientIdentity: GetMLSClientIdentityUseCase, private val isE2EIEnabled: IsE2EIEnabledUseCase, - savedStateHandle: SavedStateHandle ) : ActionsViewModel(), OtherUserProfileEventsHandler { - private val otherUserProfileNavArgs: OtherUserProfileNavArgs = savedStateHandle.navArgs() private val userId: QualifiedID = otherUserProfileNavArgs.userId private val groupConversationId: QualifiedID? = otherUserProfileNavArgs.groupConversationId diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelFactory.kt new file mode 100644 index 00000000000..a7ec1916ff4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelFactory.kt @@ -0,0 +1,65 @@ +/* + * 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.userprofile.other + +import com.wire.android.mapper.UserTypeMapper +import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveConversationRoleForUserUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.client.FetchUsersClientsFromRemoteUseCase +import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase +import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedUseCase +import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.GetMLSClientIdentityUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase +import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class OtherUserProfileScreenViewModelFactory( + private val dispatchers: DispatcherProvider, + private val observeUserInfo: ObserveUserInfoUseCase, + private val userTypeMapper: UserTypeMapper, + private val observeConversationRoleForUser: ObserveConversationRoleForUserUseCase, + private val removeMemberFromConversation: RemoveMemberFromConversationUseCase, + private val updateMemberRole: UpdateConversationMemberRoleUseCase, + private val observeClientList: ObserveClientsByUserIdUseCase, + private val fetchUsersClients: FetchUsersClientsFromRemoteUseCase, + private val getUserE2eiCertificateStatus: IsOtherUserE2EIVerifiedUseCase, + private val isOneToOneConversationCreated: IsOneToOneConversationCreatedUseCase, + private val mlsClientIdentity: GetMLSClientIdentityUseCase, + private val isE2EIEnabled: IsE2EIEnabledUseCase, +) { + fun create(args: OtherUserProfileNavArgs): OtherUserProfileScreenViewModel = OtherUserProfileScreenViewModel( + otherUserProfileNavArgs = args, + dispatchers = dispatchers, + observeUserInfo = observeUserInfo, + userTypeMapper = userTypeMapper, + observeConversationRoleForUser = observeConversationRoleForUser, + removeMemberFromConversation = removeMemberFromConversation, + updateMemberRole = updateMemberRole, + observeClientList = observeClientList, + fetchUsersClients = fetchUsersClients, + getUserE2eiCertificateStatus = getUserE2eiCertificateStatus, + isOneToOneConversationCreated = isOneToOneConversationCreated, + mlsClientIdentity = mlsClientIdentity, + isE2EIEnabled = isE2EIEnabled, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/AndroidSelfQRCodeAssetRepository.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/AndroidSelfQRCodeAssetRepository.kt new file mode 100644 index 00000000000..aafa0d8f0f4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/AndroidSelfQRCodeAssetRepository.kt @@ -0,0 +1,51 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.userprofile.qr + +import android.content.Context +import com.wire.android.appLogger +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.android.util.getTempWritableAttachmentUri +import com.wire.kalium.logic.data.asset.KaliumFileSystem +import kotlinx.coroutines.withContext +import okio.Path.Companion.toPath +import java.io.FileOutputStream + +class AndroidSelfQRCodeAssetRepository( + private val context: Context, + private val kaliumFileSystem: KaliumFileSystem, + private val dispatchers: DispatcherProvider +) : SelfQRCodeAssetRepository { + + override suspend fun saveQRCode(qrCodeImage: SelfQRCodeImage): String = withContext(dispatchers.io()) { + val tempImagePath = "${kaliumFileSystem.rootCachePath}/$TEMP_SELF_QR_FILENAME".toPath() + val qrImageUri = getTempWritableAttachmentUri(context, tempImagePath) + context.contentResolver.openFileDescriptor(qrImageUri, "rwt")?.use { fileDescriptor -> + FileOutputStream(fileDescriptor.fileDescriptor).use { fileOutputStream -> + qrCodeImage.writeTo(fileOutputStream) + fileOutputStream.flush() + } + } + appLogger.withTextTag("SelfQRCodeAssetRepository").d("Image written to: $qrImageUri") + qrImageUri.toString() + } + + companion object { + private const val TEMP_SELF_QR_FILENAME = "temp_self_qr.jpg" + } +} diff --git a/app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeAssetRepository.kt similarity index 50% rename from app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt rename to app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeAssetRepository.kt index 365ce3eac6b..c83a2411fa2 100644 --- a/app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeAssetRepository.kt @@ -15,23 +15,14 @@ * 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.initializer +package com.wire.android.ui.userprofile.qr -import android.content.Context -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent +import java.io.OutputStream -@EntryPoint -@InstallIn(SingletonComponent::class) -interface InitializerEntryPoint { +interface SelfQRCodeAssetRepository { + suspend fun saveQRCode(qrCodeImage: SelfQRCodeImage): String +} - companion object { - // a helper method to resolve the InitializerEntryPoint from the context - fun resolve(context: Context): InitializerEntryPoint { - val appContext = context.applicationContext ?: throw IllegalStateException() - return EntryPointAccessors.fromApplication(appContext, InitializerEntryPoint::class.java) - } - } +fun interface SelfQRCodeImage { + fun writeTo(outputStream: OutputStream) } diff --git a/app/src/androidTest/kotlin/com/wire/android/HiltTestApp.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt similarity index 82% rename from app/src/androidTest/kotlin/com/wire/android/HiltTestApp.kt rename to app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt index 1983d2b885d..e675247e755 100644 --- a/app/src/androidTest/kotlin/com/wire/android/HiltTestApp.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt @@ -15,9 +15,6 @@ * 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 +package com.wire.android.ui.userprofile.qr -import dagger.hilt.android.testing.CustomTestApplication - -@CustomTestApplication(BaseApp::class) -interface HiltTestApp +// Self QR code bindings are provided by the Metro graph. diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt index 0c41169b2ca..5cfb3e81d9f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt @@ -17,10 +17,8 @@ */ package com.wire.android.ui.userprofile.qr -import com.wire.android.navigation.annotation.app.WireRootDestination import android.annotation.SuppressLint import android.graphics.Bitmap -import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -55,12 +53,13 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.di.metro.metroViewModel import com.lightspark.composeqr.DotShape import com.lightspark.composeqr.QrCodeView import com.wire.android.R import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.navigation.Navigator +import com.wire.android.navigation.annotation.app.WireRootDestination import com.wire.android.navigation.style.SlideNavigationAnimation import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.colorsScheme @@ -82,7 +81,8 @@ import kotlinx.coroutines.launch @Composable fun SelfQRCodeScreen( navigator: Navigator, - viewModel: SelfQRCodeViewModel = hiltViewModel() + args: SelfQrCodeNavArgs, + viewModel: SelfQRCodeViewModel = metroViewModel { selfQRCodeViewModelFactory.create(args) } ) { if (viewModel.selfQRCodeState.hasError) { navigator.navigateBack() @@ -98,7 +98,7 @@ fun SelfQRCodeScreen( @Composable private fun SelfQRCodeContent( state: SelfQRCodeState, - shareQRAssetClick: suspend (Bitmap) -> Uri, + shareQRAssetClick: suspend (SelfQRCodeImage) -> String, trackAnalyticsEvent: (AnalyticsEvent.QrCode.Modal) -> Unit, onBackClick: () -> Unit = {} ) { @@ -219,7 +219,9 @@ private fun SelfQRCodeContent( ) coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() - val qrUri = shareQRAssetClick(bitmap.asAndroidBitmap()) + val qrUri = shareQRAssetClick { outputStream -> + bitmap.asAndroidBitmap().compress(Bitmap.CompressFormat.JPEG, QR_QUALITY_COMPRESSION, outputStream) + }.toUri() context.shareQRToProfile(qrUri) } } @@ -281,8 +283,10 @@ fun PreviewSelfQRCodeContent() { userProfileLink = "wire://user/wire.com/aaaaaaa-222-3333-4444-55555555", userAccountProfileLink = "https://account.wire.com/user-profile/?id=aaaaaaa-222-3333-4444-55555555@wire.com" ), - { "".toUri() }, + { "" }, { } ) } } + +private const val QR_QUALITY_COMPRESSION = 80 diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt index 375c15e2b60..85d11d1524a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt @@ -17,45 +17,25 @@ */ package com.wire.android.ui.userprofile.qr -import android.content.Context -import android.graphics.Bitmap -import android.net.Uri import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.wire.android.appLogger import com.wire.android.di.CurrentAccount import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.feature.analytics.model.AnalyticsEvent -import com.ramcosta.composedestinations.generated.app.navArgs -import com.wire.android.util.dispatchers.DispatcherProvider -import com.wire.android.util.getTempWritableAttachmentUri -import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okio.Path -import okio.Path.Companion.toPath -import java.io.FileOutputStream -import javax.inject.Inject -@HiltViewModel -class SelfQRCodeViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, - private val context: Context, +class SelfQRCodeViewModel( + private val selfQrCodeNavArgs: SelfQrCodeNavArgs, @CurrentAccount private val selfUserId: UserId, private val selfServerLinks: SelfServerConfigUseCase, - private val kaliumFileSystem: KaliumFileSystem, - private val dispatchers: DispatcherProvider, + private val qrAssetRepository: SelfQRCodeAssetRepository, private val analyticsManager: AnonymousAnalyticsManager ) : ViewModel() { - private val selfQrCodeNavArgs: SelfQrCodeNavArgs = savedStateHandle.navArgs() var selfQRCodeState by mutableStateOf( SelfQRCodeState( selfUserId, @@ -64,8 +44,6 @@ class SelfQRCodeViewModel @Inject constructor( ) ) private set - private val cachePath: Path - get() = kaliumFileSystem.rootCachePath init { viewModelScope.launch { @@ -73,34 +51,13 @@ class SelfQRCodeViewModel @Inject constructor( } } - suspend fun shareQRAsset(bitmap: Bitmap): Uri { - val job = viewModelScope.async { - val qrImageFile = getTempWritableQRUri(cachePath) - withContext(dispatchers.io()) { - context.contentResolver.openFileDescriptor(qrImageFile, "rwt")?.use { fileDescriptor -> - FileOutputStream(fileDescriptor.fileDescriptor) - .use { fileOutputStream -> - bitmap.compress(Bitmap.CompressFormat.JPEG, QR_QUALITY_COMPRESSION, fileOutputStream) - fileOutputStream.flush() - }.also { - appLogger.withTextTag("SelfQRCodeViewModel").d("Image written to: $qrImageFile") - } - } - } - qrImageFile - } - return job.await() - } + suspend fun shareQRAsset(qrCodeImage: SelfQRCodeImage): String = + qrAssetRepository.saveQRCode(qrCodeImage) fun trackAnalyticsEvent(event: AnalyticsEvent.QrCode.Modal) { analyticsManager.sendEvent(event) } - private suspend fun getTempWritableQRUri(tempCachePath: Path): Uri = withContext(dispatchers.io()) { - val tempImagePath = "$tempCachePath/$TEMP_SELF_QR_FILENAME".toPath() - return@withContext getTempWritableAttachmentUri(context, tempImagePath) - } - private suspend fun getServerLinks() { selfQRCodeState = when (val result = selfServerLinks()) { @@ -116,10 +73,8 @@ class SelfQRCodeViewModel @Inject constructor( ) companion object { - const val TEMP_SELF_QR_FILENAME = "temp_self_qr.jpg" const val BASE_USER_PROFILE_URL = "%s/user-profile/?id=%s" const val DIRECT_BASE_USER_PROFILE_URL = "wire://user/%s/%s" - const val QR_QUALITY_COMPRESSION = 80 } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelFactory.kt new file mode 100644 index 00000000000..581982926c8 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelFactory.kt @@ -0,0 +1,40 @@ +/* + * 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.userprofile.qr + +import com.wire.android.di.CurrentAccount +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase +import dev.zacsweers.metro.Inject + +@Inject +class SelfQRCodeViewModelFactory( + @CurrentAccount private val selfUserId: UserId, + private val selfServerLinks: SelfServerConfigUseCase, + private val qrAssetRepository: SelfQRCodeAssetRepository, + private val analyticsManager: AnonymousAnalyticsManager, +) { + fun create(args: SelfQrCodeNavArgs): SelfQRCodeViewModel = SelfQRCodeViewModel( + selfQrCodeNavArgs = args, + selfUserId = selfUserId, + selfServerLinks = selfServerLinks, + qrAssetRepository = qrAssetRepository, + analyticsManager = analyticsManager, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index dc2645dd025..bb66c447ad2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt @@ -50,10 +50,10 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.navigation.style.PopUpNavigationAnimation import com.wire.android.ui.common.R as UICommonR import com.wire.android.appLogger @@ -117,8 +117,10 @@ fun SelfUserProfileScreen( navigator: Navigator, loginTypeSelector: LoginTypeSelector, avatarPickerResultRecipient: ResultRecipient, - viewModelSelf: SelfUserProfileViewModel = hiltViewModel(), - legalHoldRequestedViewModel: LegalHoldRequestedViewModel = hiltViewModel() + viewModelSelf: SelfUserProfileViewModel = metroViewModel { selfUserProfileViewModelFactory.create() }, + legalHoldRequestedViewModel: LegalHoldRequestedViewModel = metroViewModel { + legalHoldRequestedViewModelFactory.create() + } ) { val legalHoldSubjectDialogState = rememberVisibilityState() diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index 3ea0e4c18ae..a0d6857b017 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -61,7 +61,6 @@ import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase import com.wire.kalium.logic.feature.user.ObserveValidAccountsUseCase import com.wire.kalium.logic.feature.user.UpdateSelfAvailabilityStatusUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest @@ -74,13 +73,11 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject // TODO cover this class with unit test // Suppress for now after removing mockMethodForAvatar it should not complain @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel -class SelfUserProfileViewModel @Inject constructor( +class SelfUserProfileViewModel( @CurrentAccount private val selfUserId: UserId, private val dataStore: UserDataStore, private val observeSelf: ObserveSelfUserUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelFactory.kt new file mode 100644 index 00000000000..751e5bb2ce8 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelFactory.kt @@ -0,0 +1,95 @@ +/* + * 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.userprofile.self + +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.datastore.UserDataStore +import com.wire.android.di.CurrentAccount +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.mapper.OtherAccountMapper +import com.wire.android.notification.WireNotificationManager +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.client.IsProfileQRCodeEnabledUseCase +import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase +import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase +import com.wire.kalium.logic.feature.team.SyncSelfTeamInfoUseCase +import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserWithTeamUseCase +import com.wire.kalium.logic.feature.user.ObserveValidAccountsUseCase +import com.wire.kalium.logic.feature.user.UpdateSelfAvailabilityStatusUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class SelfUserProfileViewModelFactory( + @CurrentAccount private val selfUserId: UserId, + private val dataStore: UserDataStore, + private val observeSelf: ObserveSelfUserUseCase, + private val observeSelfUserWithTeam: ObserveSelfUserWithTeamUseCase, + private val syncSelfTeamInfo: SyncSelfTeamInfoUseCase, + private val canMigrateFromPersonalToTeam: CanMigrateFromPersonalToTeamUseCase, + private val observeValidAccounts: ObserveValidAccountsUseCase, + private val updateStatus: UpdateSelfAvailabilityStatusUseCase, + private val logout: LogoutUseCase, + private val observeLegalHoldStatusForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, + private val dispatchers: DispatcherProvider, + private val otherAccountMapper: OtherAccountMapper, + private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, + private val accountSwitch: AccountSwitchUseCase, + private val endCall: EndCallUseCase, + private val isReadOnlyAccount: IsReadOnlyAccountUseCase, + private val notificationManager: WireNotificationManager, + private val globalDataStore: GlobalDataStore, + private val qualifiedIdMapper: QualifiedIdMapper, + private val anonymousAnalyticsManager: AnonymousAnalyticsManager, + private val getTeamUrl: GetTeamUrlUseCase, + private val isProfileQRCodeEnabled: IsProfileQRCodeEnabledUseCase, +) { + fun create(): SelfUserProfileViewModel = SelfUserProfileViewModel( + selfUserId = selfUserId, + dataStore = dataStore, + observeSelf = observeSelf, + observeSelfUserWithTeam = observeSelfUserWithTeam, + syncSelfTeamInfo = syncSelfTeamInfo, + canMigrateFromPersonalToTeam = canMigrateFromPersonalToTeam, + observeValidAccounts = observeValidAccounts, + updateStatus = updateStatus, + logout = logout, + observeLegalHoldStatusForSelfUser = observeLegalHoldStatusForSelfUser, + dispatchers = dispatchers, + otherAccountMapper = otherAccountMapper, + observeEstablishedCalls = observeEstablishedCalls, + accountSwitch = accountSwitch, + endCall = endCall, + isReadOnlyAccount = isReadOnlyAccount, + notificationManager = notificationManager, + globalDataStore = globalDataStore, + qualifiedIdMapper = qualifiedIdMapper, + anonymousAnalyticsManager = anonymousAnalyticsManager, + getTeamUrl = getTeamUrl, + isProfileQRCodeEnabled = isProfileQRCodeEnabled, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsNavArgsProvider.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsNavArgsProvider.kt new file mode 100644 index 00000000000..64c3a3da7ed --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsNavArgsProvider.kt @@ -0,0 +1,29 @@ +/* + * 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.userprofile.service + +import androidx.lifecycle.SavedStateHandle +import com.ramcosta.composedestinations.generated.app.navArgs +import javax.inject.Inject + +class ServiceDetailsNavArgsProvider @Inject constructor( + private val savedStateHandle: SavedStateHandle, +) { + fun serviceDetailsNavArgs(): ServiceDetailsNavArgs = savedStateHandle.navArgs() +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt index fa7be0e4c33..b95284612c4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsScreen.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.ClickBlockParams import com.wire.android.model.NameBasedAvatar import com.wire.android.model.UserAvatarData @@ -69,7 +69,10 @@ import com.wire.kalium.logic.data.service.ServiceDetails @Composable fun ServiceDetailsScreen( navigator: Navigator, - viewModel: ServiceDetailsViewModel = hiltViewModel() + args: ServiceDetailsNavArgs, + viewModel: ServiceDetailsViewModel = metroViewModel { + serviceDetailsViewModelFactory.create(args) + } ) { val snackbarHostState = LocalSnackbarHostState.current val context = LocalContext.current diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt index 00940bb6f9a..514e4393d55 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt @@ -20,15 +20,13 @@ package com.wire.android.ui.userprofile.service import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.di.CurrentAccount import com.wire.android.model.ImageAsset import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveConversationRoleForUserUseCase -import com.ramcosta.composedestinations.generated.app.navArgs -import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.AppsUtil +import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.QualifiedID @@ -46,7 +44,6 @@ import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageU import com.wire.kalium.logic.feature.service.GetServiceByIdUseCase import com.wire.kalium.logic.feature.service.ObserveIsServiceMemberResult import com.wire.kalium.logic.feature.service.ObserveIsServiceMemberUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -58,11 +55,9 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject @Suppress("LongParameterList") -@HiltViewModel -class ServiceDetailsViewModel @Inject constructor( +class ServiceDetailsViewModel( private val dispatchers: DispatcherProvider, @CurrentAccount private val selfUserId: UserId, private val getServiceById: GetServiceByIdUseCase, @@ -75,11 +70,9 @@ class ServiceDetailsViewModel @Inject constructor( private val removeMemberFromConversation: RemoveMemberFromConversationUseCase, private val addServiceToConversation: AddServiceToConversationUseCase, private val addMemberToConversation: AddMemberToConversationUseCase, - savedStateHandle: SavedStateHandle + private val serviceDetailsNavArgs: ServiceDetailsNavArgs ) : ViewModel() { - private val serviceDetailsNavArgs: ServiceDetailsNavArgs = savedStateHandle.navArgs() - private val serviceId: ServiceId = serviceDetailsNavArgs.id.serviceId private val conversationId: QualifiedID? = serviceDetailsNavArgs.conversationId diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelFactory.kt new file mode 100644 index 00000000000..d7dac1fa73e --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelFactory.kt @@ -0,0 +1,66 @@ +/* + * 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.userprofile.service + +import com.wire.android.di.CurrentAccount +import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveConversationRoleForUserUseCase +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.app.GetAppByIdUseCase +import com.wire.kalium.logic.feature.app.ObserveIsAppMemberUseCase +import com.wire.kalium.logic.feature.conversation.AddMemberToConversationUseCase +import com.wire.kalium.logic.feature.conversation.AddServiceToConversationUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase +import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppsAllowedForUsageUseCase +import com.wire.kalium.logic.feature.service.GetServiceByIdUseCase +import com.wire.kalium.logic.feature.service.ObserveIsServiceMemberUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class ServiceDetailsViewModelFactory( + private val dispatchers: DispatcherProvider, + @CurrentAccount private val selfUserId: UserId, + private val getServiceById: GetServiceByIdUseCase, + private val getAppById: GetAppByIdUseCase, + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val observeIsServiceMember: ObserveIsServiceMemberUseCase, + private val observeIsAppMember: ObserveIsAppMemberUseCase, + private val observeIsAppsAllowedForUsage: ObserveIsAppsAllowedForUsageUseCase, + private val observeConversationRoleForUser: ObserveConversationRoleForUserUseCase, + private val removeMemberFromConversation: RemoveMemberFromConversationUseCase, + private val addServiceToConversation: AddServiceToConversationUseCase, + private val addMemberToConversation: AddMemberToConversationUseCase, +) { + fun create(args: ServiceDetailsNavArgs): ServiceDetailsViewModel = ServiceDetailsViewModel( + dispatchers = dispatchers, + selfUserId = selfUserId, + getServiceById = getServiceById, + getAppById = getAppById, + observeConversationDetails = observeConversationDetails, + observeIsServiceMember = observeIsServiceMember, + observeIsAppMember = observeIsAppMember, + observeIsAppsAllowedForUsage = observeIsAppsAllowedForUsage, + observeConversationRoleForUser = observeConversationRoleForUser, + removeMemberFromConversation = removeMemberFromConversation, + addServiceToConversation = addServiceToConversation, + addMemberToConversation = addMemberToConversation, + serviceDetailsNavArgs = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt index 3d894f2d504..cd32a25e69f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt @@ -29,12 +29,9 @@ import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamResult import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamUseCase -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class TeamMigrationViewModel @Inject constructor( +class TeamMigrationViewModel( private val anonymousAnalyticsManager: AnonymousAnalyticsManager, private val migrateFromPersonalToTeam: MigrateFromPersonalToTeamUseCase, private val observeSelfUser: ObserveSelfUserUseCase, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelFactory.kt new file mode 100644 index 00000000000..c01240ec178 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelFactory.kt @@ -0,0 +1,42 @@ +/* + * 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.userprofile.teammigration + +import com.wire.android.datastore.UserDataStore +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamUseCase +import dev.zacsweers.metro.Inject + +@Inject +class TeamMigrationViewModelFactory( + private val anonymousAnalyticsManager: AnonymousAnalyticsManager, + private val migrateFromPersonalToTeam: MigrateFromPersonalToTeamUseCase, + private val observeSelfUser: ObserveSelfUserUseCase, + private val dataStore: UserDataStore, + private val getTeamUrl: GetTeamUrlUseCase, +) { + fun create(): TeamMigrationViewModel = TeamMigrationViewModel( + anonymousAnalyticsManager = anonymousAnalyticsManager, + migrateFromPersonalToTeam = migrateFromPersonalToTeam, + observeSelfUser = observeSelfUser, + dataStore = dataStore, + getTeamUrl = getTeamUrl, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/util/FileManager.kt b/app/src/main/kotlin/com/wire/android/util/FileManager.kt index 46abf904a2e..752cdf2ca26 100644 --- a/app/src/main/kotlin/com/wire/android/util/FileManager.kt +++ b/app/src/main/kotlin/com/wire/android/util/FileManager.kt @@ -25,7 +25,7 @@ import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.util.dispatchers.DefaultDispatcherProvider import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.asset.AttachmentType -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext import kotlinx.coroutines.withContext import okio.Path import okio.Path.Companion.toPath diff --git a/app/src/main/kotlin/com/wire/android/util/ScreenStateObserver.kt b/app/src/main/kotlin/com/wire/android/util/ScreenStateObserver.kt index 309611539a8..3039eaeb841 100644 --- a/app/src/main/kotlin/com/wire/android/util/ScreenStateObserver.kt +++ b/app/src/main/kotlin/com/wire/android/util/ScreenStateObserver.kt @@ -23,7 +23,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.PowerManager -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject diff --git a/app/src/main/kotlin/com/wire/android/util/UserAgentProvider.kt b/app/src/main/kotlin/com/wire/android/util/UserAgentProvider.kt index 93d4d7f5243..ea904cb832f 100644 --- a/app/src/main/kotlin/com/wire/android/util/UserAgentProvider.kt +++ b/app/src/main/kotlin/com/wire/android/util/UserAgentProvider.kt @@ -19,7 +19,7 @@ package com.wire.android.util import android.content.Context import android.os.Build -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/kotlin/com/wire/android/util/lifecycle/IntentsProcessor.kt b/app/src/main/kotlin/com/wire/android/util/lifecycle/IntentsProcessor.kt index 22008cb68e3..838386280a3 100644 --- a/app/src/main/kotlin/com/wire/android/util/lifecycle/IntentsProcessor.kt +++ b/app/src/main/kotlin/com/wire/android/util/lifecycle/IntentsProcessor.kt @@ -36,12 +36,15 @@ class IntentsProcessor @Inject internal constructor( ) { companion object { - private const val AUTOMATED_LOGIN = "automated_login" + const val AUTOMATED_LOGIN = "automated_login" internal const val SKIP_SIGNATURE_VERIFICATION_TOKEN = NomadIntentSignatureValidator.SKIP_SIGNATURE_VERIFICATION_TOKEN } + suspend operator fun invoke(intent: Intent?): AutomatedLoginViaSSO? = + parseAutomatedLogin(intent?.getStringExtra(AUTOMATED_LOGIN)) + @Suppress("ReturnCount") - suspend operator fun invoke(intent: Intent?): AutomatedLoginViaSSO? { + suspend fun parseAutomatedLogin(automatedLogin: String?): AutomatedLoginViaSSO? { @Serializable data class Parameters( val backendConfig: String? = null, @@ -51,9 +54,7 @@ class IntentsProcessor @Inject internal constructor( ) val parsed = runCatching { - intent - ?.getStringExtra(AUTOMATED_LOGIN) - ?.let { Json.decodeFromString(it) } + automatedLogin?.let { Json.decodeFromString(it) } }.getOrNull() ?: return null if (parsed.nomadProfilesHost.isNullOrEmpty()) { diff --git a/app/src/main/kotlin/com/wire/android/util/ui/UiTextResolver.kt b/app/src/main/kotlin/com/wire/android/util/ui/UiTextResolver.kt index bfd5f24beec..54dcd7b0801 100644 --- a/app/src/main/kotlin/com/wire/android/util/ui/UiTextResolver.kt +++ b/app/src/main/kotlin/com/wire/android/util/ui/UiTextResolver.kt @@ -19,7 +19,7 @@ package com.wire.android.util.ui import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext import java.util.Locale import javax.inject.Inject diff --git a/app/src/main/kotlin/com/wire/android/workmanager/worker/AssetUploadObserverWorker.kt b/app/src/main/kotlin/com/wire/android/workmanager/worker/AssetUploadObserverWorker.kt index 9ac5039da15..9cf518658e0 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/worker/AssetUploadObserverWorker.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/worker/AssetUploadObserverWorker.kt @@ -34,8 +34,6 @@ import com.wire.android.notification.openAppPendingIntent import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.session.CurrentSessionResult -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first @@ -46,9 +44,9 @@ import kotlinx.coroutines.flow.map * A Worker that observes asset uploads and only completes when there are no uploads in progress. * This is required to let the network operations running when the app is in the background. */ -class AssetUploadObserverWorker @AssistedInject constructor( - @Assisted appContext: Context, - @Assisted workerParams: WorkerParameters, +class AssetUploadObserverWorker( + appContext: Context, + workerParams: WorkerParameters, private val coreLogic: CoreLogic, private val notificationChannelsManager: NotificationChannelsManager, ) : CoroutineWorker(appContext, workerParams) { diff --git a/app/src/main/kotlin/com/wire/android/workmanager/worker/DeleteConversationLocallyWorker.kt b/app/src/main/kotlin/com/wire/android/workmanager/worker/DeleteConversationLocallyWorker.kt index c0da71761e7..ba56f9c1352 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/worker/DeleteConversationLocallyWorker.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/worker/DeleteConversationLocallyWorker.kt @@ -19,7 +19,6 @@ package com.wire.android.workmanager.worker import android.content.Context import androidx.core.app.NotificationCompat -import androidx.hilt.work.HiltWorker import androidx.work.Constraints import androidx.work.CoroutineWorker import androidx.work.ExistingWorkPolicy @@ -42,8 +41,6 @@ import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase import com.wire.kalium.logic.feature.session.DoesValidSessionExistResult -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapNotNull @@ -60,10 +57,9 @@ import kotlinx.coroutines.flow.mapNotNull * @param coreLogic A utility object that handles core application logic, such as session and conversation management. * @param notificationChannelsManager Manages notification channels for the application. */ -@HiltWorker -class DeleteConversationLocallyWorker @AssistedInject constructor( - @Assisted appContext: Context, - @Assisted workerParams: WorkerParameters, +class DeleteConversationLocallyWorker( + appContext: Context, + workerParams: WorkerParameters, private val coreLogic: CoreLogic, private val notificationChannelsManager: NotificationChannelsManager, ) : CoroutineWorker(appContext, workerParams) { diff --git a/app/src/main/kotlin/com/wire/android/workmanager/worker/NotificationFetchWorker.kt b/app/src/main/kotlin/com/wire/android/workmanager/worker/NotificationFetchWorker.kt index 7f4065fd65c..4330066f897 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/worker/NotificationFetchWorker.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/worker/NotificationFetchWorker.kt @@ -20,7 +20,6 @@ package com.wire.android.workmanager.worker import android.content.Context import androidx.core.app.NotificationCompat -import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters @@ -29,14 +28,10 @@ import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.NotificationConstants import com.wire.android.notification.NotificationIds import com.wire.android.notification.WireNotificationManager -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -@HiltWorker -class NotificationFetchWorker -@AssistedInject constructor( - @Assisted appContext: Context, - @Assisted workerParams: WorkerParameters, +class NotificationFetchWorker( + appContext: Context, + workerParams: WorkerParameters, private val wireNotificationManager: WireNotificationManager, private val notificationChannelsManager: NotificationChannelsManager ) : CoroutineWorker(appContext, workerParams) { diff --git a/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt b/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt index c957eef1bfe..3f1628c6274 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt @@ -21,7 +21,6 @@ package com.wire.android.workmanager.worker import android.content.Context import androidx.core.app.NotificationCompat -import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ForegroundInfo @@ -38,17 +37,13 @@ import com.wire.android.notification.openAppPendingIntent import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker.Companion.NAME import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker.Companion.TAG import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker.Companion.WORK_INTERVAL -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import kotlinx.coroutines.coroutineScope import kotlin.time.Duration.Companion.hours import kotlin.time.toJavaDuration -@HiltWorker -class PersistentWebsocketCheckWorker -@AssistedInject constructor( - @Assisted private val appContext: Context, - @Assisted private val workerParams: WorkerParameters, +class PersistentWebsocketCheckWorker( + private val appContext: Context, + private val workerParams: WorkerParameters, private val startPersistentWebsocketIfNecessary: StartPersistentWebsocketIfNecessaryUseCase, private val notificationChannelsManager: NotificationChannelsManager ) : CoroutineWorker(appContext, workerParams) { diff --git a/app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt b/app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt index 550ada2a96a..916a884f4a9 100644 --- a/app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt +++ b/app/src/nonfree/kotlin/com/wire/android/services/WireFirebaseMessagingService.kt @@ -30,30 +30,25 @@ import com.google.firebase.messaging.RemoteMessage import com.wire.android.BuildConfig import com.wire.android.appLogger import com.wire.android.di.KaliumCoreLogic +import com.wire.android.di.metro.createWireMetroGraph import com.wire.android.util.NetworkUtil import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.workmanager.worker.NotificationFetchWorker import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.feature.notificationToken.Result -import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import java.util.Locale -import javax.inject.Inject -@AndroidEntryPoint class WireFirebaseMessagingService : FirebaseMessagingService() { - @Inject @KaliumCoreLogic lateinit var coreLogic: CoreLogic - @Inject lateinit var networkUtil: NetworkUtil - @Inject lateinit var dispatcherProvider: DispatcherProvider private val scope by lazy { @@ -63,6 +58,7 @@ class WireFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) + injectDependencies() appLogger.i( String.format( Locale.US, @@ -79,6 +75,15 @@ class WireFirebaseMessagingService : FirebaseMessagingService() { appLogger.i("$TAG: onMessageReceived End") } + private fun injectDependencies() { + if (::dispatcherProvider.isInitialized) return + + val graph = createWireMetroGraph(this) + coreLogic = graph.coreLogic + networkUtil = graph.networkUtil + dispatcherProvider = graph.dispatcherProvider + } + private fun enqueueNotificationFetchWorker(userId: String) { val requestBuilder = OneTimeWorkRequestBuilder() .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) @@ -120,6 +125,7 @@ class WireFirebaseMessagingService : FirebaseMessagingService() { override fun onNewToken(token: String) { super.onNewToken(token) + injectDependencies() scope.launch { coreLogic.globalScope { saveNotificationToken(token, "GCM", BuildConfig.FIREBASE_PUSH_SENDER_ID) @@ -135,7 +141,9 @@ class WireFirebaseMessagingService : FirebaseMessagingService() { } override fun onDestroy() { - scope.cancel() + if (::dispatcherProvider.isInitialized) { + scope.cancel() + } appLogger.i("$TAG: onDestroy") super.onDestroy() } diff --git a/app/src/nonfree/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt b/app/src/nonfree/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt index 378b2a18fd7..a29bc2da89a 100644 --- a/app/src/nonfree/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt +++ b/app/src/nonfree/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerHelperFlavor.kt @@ -25,10 +25,11 @@ import com.google.android.gms.tasks.CancellationTokenSource import com.wire.android.AppJsonStyledLogger import com.wire.android.util.extension.isGoogleServicesAvailable import com.wire.kalium.logger.KaliumLogLevel +import dev.zacsweers.metro.Inject as MetroInject import kotlinx.coroutines.tasks.await import javax.inject.Inject -class LocationPickerHelperFlavor @Inject constructor( +class LocationPickerHelperFlavor @Inject @MetroInject constructor( private val context: Context, private val geocoderHelper: GeocoderHelper, private val locationPickerHelper: LocationPickerHelper, diff --git a/app/src/test/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiverTest.kt b/app/src/test/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiverTest.kt index b82d376827d..f91e641e375 100644 --- a/app/src/test/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiverTest.kt +++ b/app/src/test/kotlin/com/wire/android/notification/broadcastreceivers/NomadLogoutReceiverTest.kt @@ -43,6 +43,7 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.runs import io.mockk.verify import kotlinx.coroutines.test.advanceUntilIdle @@ -164,15 +165,26 @@ class NomadLogoutReceiverTest { @MockK lateinit var isCurrentSessionNomadAccount: IsCurrentSessionNomadAccountUseCase + @MockK + lateinit var dependencies: BroadcastReceiverDependencies + val context = mockk(relaxed = true) val receiver = NomadLogoutReceiver() init { MockKAnnotations.init(this, relaxUnitFun = true) + mockkStatic("com.wire.android.notification.broadcastreceivers.BroadcastReceiverDependenciesKt") every { context.applicationContext } returns context every { context.packageName } returns "com.wire.android" every { context.startActivity(any()) } just runs + every { context.broadcastReceiverDependencies } returns dependencies + + every { dependencies.coreLogic() } returns coreLogic + every { dependencies.currentSession() } returns currentSession + every { dependencies.accountSwitch() } returns accountSwitch + every { dependencies.switchAccountObserver() } returns switchAccountObserver + every { dependencies.nomadProfilesFeatureConfig() } returns nomadProfilesFeatureConfig every { userSessionScope.logout } returns logoutUseCase coEvery { logoutUseCase(any(), any()) } returns Unit @@ -186,11 +198,6 @@ class NomadLogoutReceiverTest { } fun arrange(): Arrangement { - receiver.coreLogic = coreLogic - receiver.currentSession = currentSession - receiver.accountSwitch = accountSwitch - receiver.switchAccountObserver = switchAccountObserver - receiver.nomadProfilesFeatureConfig = nomadProfilesFeatureConfig return this } diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 7a3fb4fc1eb..fe257bf47d0 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -20,7 +20,6 @@ package com.wire.android.ui -import android.content.Intent import androidx.work.Operation import androidx.work.WorkManager import app.cash.turbine.test @@ -30,7 +29,6 @@ import com.wire.android.assertions.shouldBeEqualTo import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NomadProfilesFeatureConfig import com.wire.android.config.TestDispatcherProvider -import com.wire.android.config.mockUri import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.IsProfileQRCodeEnabledUseCaseProvider import com.wire.android.di.ObserveIfE2EIRequiredDuringLoginUseCaseProvider @@ -52,12 +50,10 @@ import com.wire.android.ui.joinConversation.JoinConversationViaCodeState import com.wire.android.ui.theme.ThemeOption import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager -import com.wire.android.util.deeplink.DeepLinkProcessor import com.wire.android.util.deeplink.DeepLinkResult import com.wire.android.util.deeplink.LoginType import com.wire.android.util.lifecycle.AutomatedLoginManager import com.wire.android.util.lifecycle.AutomatedLoginViaSSO -import com.wire.android.util.lifecycle.IntentsProcessor import com.wire.android.util.newServerConfig import com.wire.kalium.common.error.NetworkFailure import com.wire.kalium.logic.CoreLogic @@ -158,11 +154,28 @@ class WireActivityViewModelTest { viewModel.actions.test { viewModel.handleDeepLink(mockedIntent()) - coVerify(exactly = 1) { arrangement.deepLinkProcessor.invoke(any(), any()) } + coVerify(exactly = 1) { arrangement.intentGateway.parseDeepLink(any()) } assertEquals(OnSSOLogin(result), expectMostRecentItem()) } } + @Test + fun `given parsed intent content, when handling deep link, then content is delegated to gateway`() = runTest { + val intentContent = WireActivityIntentContent( + dataUri = "wire://conversation/domain/conversation-id", + action = "android.intent.action.VIEW", + automatedLogin = null, + ) + val (arrangement, viewModel) = Arrangement() + .withDeepLinkResult(DeepLinkResult.Unknown) + .arrange() + + viewModel.handleDeepLink(intentContent) + advanceUntilIdle() + + coVerify(exactly = 1) { arrangement.intentGateway.parseDeepLink(intentContent) } + } + @Test fun `given intent with correct ServerConfig json, when no network is present, then initialAppState is LOGGED_IN and no network dialog is shown`() = runTest { @@ -871,7 +884,7 @@ class WireActivityViewModelTest { assertEquals(true, handled) assertEquals(false, arrangement.automatedLoginManager.pendingMoveToBackgroundAfterSync) - coVerify(exactly = 1) { arrangement.intentsProcessor(any()) } + coVerify(exactly = 1) { arrangement.intentGateway.parseAutomatedLogin(any()) } coVerify(exactly = 0) { arrangement.isNomadProfilesEnabledUseCase.invoke() } coVerify(exactly = 0) { arrangement.observeSessionsUseCase.invoke() } expectNoEvents() @@ -1016,12 +1029,11 @@ class WireActivityViewModelTest { MockKAnnotations.init(this, relaxUnitFun = true) // Default empty values - mockUri() coEvery { monitorSyncWorkUseCase() } returns Unit coEvery { currentSessionFlow() } returns flowOf() coEvery { getServerConfigUseCase(any()) } returns GetServerConfigResult.Success(newServerConfig(1).links) - coEvery { deepLinkProcessor(any(), any()) } returns DeepLinkResult.Unknown - coEvery { intentsProcessor(any()) } returns null + coEvery { intentGateway.parseDeepLink(any()) } returns DeepLinkResult.Unknown + coEvery { intentGateway.parseAutomatedLogin(any()) } returns null coEvery { observeSessionsUseCase.invoke() } returns flowOf(GetAllSessionsResult.Failure.NoSessionFound) every { observeSyncStateUseCaseProviderFactory.create(any()).observeSyncState } returns observeSyncStateUseCase every { observeSyncStateUseCase() } returns emptyFlow() @@ -1061,10 +1073,7 @@ class WireActivityViewModelTest { lateinit var getServerConfigUseCase: GetServerConfigUseCase @MockK - lateinit var deepLinkProcessor: DeepLinkProcessor - - @MockK - lateinit var intentsProcessor: IntentsProcessor + lateinit var intentGateway: WireActivityIntentGateway @MockK lateinit var observeSessionsUseCase: ObserveSessionsUseCase @@ -1148,8 +1157,7 @@ class WireActivityViewModelTest { currentSessionFlow = { currentSessionFlow }, doesValidSessionExist = { doesValidSessionExist }, getServerConfigUseCase = { getServerConfigUseCase }, - deepLinkProcessor = { deepLinkProcessor }, - intentsProcessor = { intentsProcessor }, + intentGateway = { intentGateway }, observeSessions = { observeSessionsUseCase }, accountSwitch = { switchAccount }, servicesManager = { servicesManager }, @@ -1205,7 +1213,7 @@ class WireActivityViewModelTest { } fun withDeepLinkResult(result: DeepLinkResult): Arrangement { - coEvery { deepLinkProcessor(any(), any()) } returns result + coEvery { intentGateway.parseDeepLink(any()) } returns result return this } @@ -1303,7 +1311,7 @@ class WireActivityViewModelTest { ssoCode: String? = null, backendConfig: String? = null, ): Arrangement = apply { - coEvery { intentsProcessor(any()) } returns when { + coEvery { intentGateway.parseAutomatedLogin(any()) } returns when { ssoCode == null || backendConfig == null -> null else -> AutomatedLoginViaSSO( ssoCode = ssoCode, @@ -1340,13 +1348,12 @@ class WireActivityViewModelTest { TEST_ACCOUNT_INFO.copy(userId = USER_ID.copy("user_$i")) } - private fun mockedIntent(isFromHistory: Boolean = false): Intent { - return mockk().also { - every { it.data } returns mockk() - every { it.flags } returns if (isFromHistory) Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY else 0 - every { it.action } returns null - } - } + private fun mockedIntent(): WireActivityIntentContent = + WireActivityIntentContent( + dataUri = "wire://conversation/domain/conversation-id", + action = null, + automatedLogin = null, + ) val ongoingCall = Call( CommonTopAppBarViewModelTest.conversationId, diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/LoginViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/LoginViewModelTest.kt index 8a391b7327d..537a55a2d55 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/LoginViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/LoginViewModelTest.kt @@ -18,14 +18,13 @@ package com.wire.android.ui.authentication -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.ClientScopeProvider import com.wire.android.ui.authentication.login.LoginNavArgs import com.wire.android.ui.authentication.login.LoginPasswordPath import com.wire.android.ui.authentication.login.LoginViewModel -import com.ramcosta.composedestinations.generated.app.navArgs +import com.wire.android.ui.authentication.login.LoginViewModelExtension import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.data.id.QualifiedID @@ -47,9 +46,6 @@ class LoginViewModelTest { @MockK private lateinit var qualifiedIdMapper: QualifiedIdMapper - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK private lateinit var userDataStoreProvider: UserDataStoreProvider @@ -62,14 +58,12 @@ class LoginViewModelTest { fun setup() { MockKAnnotations.init(this) every { qualifiedIdMapper.fromStringToQualifiedID(any()) } returns QualifiedID("", "") - every { savedStateHandle.navArgs() } returns LoginNavArgs( - loginPasswordPath = LoginPasswordPath(ServerConfig.STAGING) - ) loginViewModel = LoginViewModel( - savedStateHandle, + LoginNavArgs(loginPasswordPath = LoginPasswordPath(ServerConfig.STAGING)), clientScopeProviderFactory, userDataStoreProvider, coreLogic, + LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), ServerConfig.STAGING ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelTest.kt index 863fd196207..6556701f3bf 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelTest.kt @@ -19,7 +19,6 @@ package com.wire.android.ui.authentication.create.details import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.lifecycle.SavedStateHandle import com.wire.android.assertions.shouldBeEqualTo import com.wire.android.assertions.shouldBeInstanceOf import com.wire.android.config.CoroutineTestExtension @@ -27,13 +26,11 @@ import com.wire.android.config.NavigationTestExtension import com.wire.android.config.SnapshotExtension import com.wire.android.ui.authentication.create.common.CreateAccountFlowType import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.auth.ValidatePasswordResult import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle @@ -47,7 +44,7 @@ class CreateAccountDetailsViewModelTest { @Test fun `given invalid password, when executing, then show error`() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withValidatePasswordResult(ValidatePasswordResult.Invalid()) .arrange() viewModel.passwordTextState.setTextAndPlaceCursorAtEnd("password") @@ -62,7 +59,7 @@ class CreateAccountDetailsViewModelTest { @Test fun `given passwords do not match, when executing, then show error`() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withValidatePasswordResult(ValidatePasswordResult.Valid) .arrange() viewModel.passwordTextState.setTextAndPlaceCursorAtEnd("password") @@ -78,7 +75,7 @@ class CreateAccountDetailsViewModelTest { @Test fun `given valid passwords, when executing, then show success`() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withValidatePasswordResult(ValidatePasswordResult.Valid) .arrange() viewModel.passwordTextState.setTextAndPlaceCursorAtEnd("password") @@ -92,22 +89,19 @@ class CreateAccountDetailsViewModelTest { } private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var validatePasswordUseCase: ValidatePasswordUseCase + private val createAccountNavArgs = CreateAccountNavArgs(CreateAccountFlowType.CreatePersonalAccount) + init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.navArgs() } returns - CreateAccountNavArgs(CreateAccountFlowType.CreatePersonalAccount) } fun withValidatePasswordResult(result: ValidatePasswordResult) = apply { coEvery { validatePasswordUseCase(any()) } returns result } - fun arrange() = this to CreateAccountDetailsViewModel(savedStateHandle, validatePasswordUseCase, ServerConfig.STAGING) + fun arrange() = this to CreateAccountDetailsViewModel(createAccountNavArgs, validatePasswordUseCase, ServerConfig.STAGING) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelTest.kt index 61e962a9890..24ab4ca8053 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelTest.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.authentication.create.email -import androidx.lifecycle.SavedStateHandle import com.wire.android.assertions.shouldBeEqualTo import com.wire.android.assertions.shouldBeInstanceOf import com.wire.android.config.CoroutineTestExtension @@ -26,7 +25,6 @@ import com.wire.android.config.NavigationTestExtension import com.wire.android.config.SnapshotExtension import com.wire.android.ui.authentication.create.common.CreateAccountFlowType import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.auth.AuthenticationScope @@ -36,7 +34,6 @@ import com.wire.kalium.logic.feature.register.RequestActivationCodeResult import com.wire.kalium.logic.feature.register.RequestActivationCodeUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle @@ -50,7 +47,7 @@ class CreateAccountEmailViewModelTest { @Test fun `given request code error, when terms accepted, then show error`() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withRequestActivationCodeResult(RequestActivationCodeResult.Failure.InvalidEmail) .arrange() @@ -63,7 +60,7 @@ class CreateAccountEmailViewModelTest { @Test fun `given request code success, when terms accepted, then show success`() = runTest { - val (arrangement, viewModel) = Arrangement() + val (_, viewModel) = Arrangement() .withRequestActivationCodeResult(RequestActivationCodeResult.Success) .arrange() @@ -75,9 +72,6 @@ class CreateAccountEmailViewModelTest { } private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var validateEmailUseCase: ValidateEmailUseCase @@ -93,10 +87,10 @@ class CreateAccountEmailViewModelTest { @MockK lateinit var requestActivationCodeUseCase: RequestActivationCodeUseCase + private val createAccountNavArgs = CreateAccountNavArgs(CreateAccountFlowType.CreatePersonalAccount) + init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.navArgs() } returns - CreateAccountNavArgs(CreateAccountFlowType.CreatePersonalAccount) coEvery { coreLogic.versionedAuthenticationScope(any()) } returns autoVersionAuthScopeUseCase coEvery { autoVersionAuthScopeUseCase(any()) } returns AutoVersionAuthScopeUseCase.Result.Success(versionedAuthenticationScope) @@ -107,6 +101,6 @@ class CreateAccountEmailViewModelTest { coEvery { requestActivationCodeUseCase(any()) } returns result } - fun arrange() = this to CreateAccountEmailViewModel(savedStateHandle, validateEmailUseCase, coreLogic, ServerConfig.STAGING) + fun arrange() = this to CreateAccountEmailViewModel(createAccountNavArgs, validateEmailUseCase, coreLogic, ServerConfig.STAGING) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/SavedStateLoginSavedInputStoreTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/SavedStateLoginSavedInputStoreTest.kt new file mode 100644 index 00000000000..292c772bb0e --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/SavedStateLoginSavedInputStoreTest.kt @@ -0,0 +1,46 @@ +/* + * 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.authentication.login + +import androidx.lifecycle.SavedStateHandle +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +class SavedStateLoginSavedInputStoreTest { + + @Test + fun givenEmptySavedState_whenStoreIsCreated_thenSavedInputsAreNull() { + val store: LoginSavedInputStore = SavedStateLoginSavedInputStore(SavedStateHandle()) + + assertNull(store.userIdentifier) + assertNull(store.ssoCode) + } + + @Test + fun givenSavedInputs_whenValuesAreUpdated_thenValuesCanBeReadThroughPort() { + val store: LoginSavedInputStore = SavedStateLoginSavedInputStore(SavedStateHandle()) + + store.userIdentifier = "user@example.com" + store.ssoCode = "wire-sso-code" + + assertEquals("user@example.com", store.userIdentifier) + assertEquals("wire-sso-code", store.ssoCode) + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt index 727e11dd517..dfeea04efd9 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt @@ -21,7 +21,6 @@ package com.wire.android.ui.authentication.login.email import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.android.assertions.shouldBeEqualTo import com.wire.android.assertions.shouldBeInstanceOf @@ -37,8 +36,8 @@ import com.wire.android.di.ClientScopeProvider import com.wire.android.framework.TestClient import com.wire.android.ui.authentication.login.LoginNavArgs import com.wire.android.ui.authentication.login.LoginPasswordPath +import com.wire.android.ui.authentication.login.LoginSavedInputStore import com.wire.android.ui.authentication.login.LoginState -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.EMPTY import com.wire.android.util.newServerConfig import com.wire.android.util.ui.CountdownTimer @@ -815,7 +814,7 @@ class LoginEmailViewModelTest { internal lateinit var getOrRegisterClientUseCase: GetOrRegisterClientUseCase @MockK - internal lateinit var savedStateHandle: SavedStateHandle + internal lateinit var savedInputStore: LoginSavedInputStore @MockK internal lateinit var qualifiedIdMapper: QualifiedIdMapper @@ -853,17 +852,14 @@ class LoginEmailViewModelTest { init { MockKAnnotations.init(this, relaxUnitFun = true) mockUri() - every { savedStateHandle.get(any()) } returns null + every { savedInputStore.userIdentifier } returns null every { qualifiedIdMapper.fromStringToQualifiedID(any()) } returns USER_ID - every { savedStateHandle.set(any(), any()) } returns Unit + every { savedInputStore.userIdentifier = any() } returns Unit every { coreLogic.getGlobalScope().validateEmailUseCase } returns validateEmailUseCase every { coreLogic.getSessionScope(any()).users } returns userScope every { userScope.persistSelfUserEmail } returns persistSelfUserEmailUseCase every { clientScopeProviderFactory.create(any()).clientScope } returns clientScope every { clientScope.getOrRegister } returns getOrRegisterClientUseCase - every { savedStateHandle.navArgs() } returns LoginNavArgs( - loginPasswordPath = LoginPasswordPath(newServerConfig(1).links) - ) coEvery { autoVersionAuthScopeUseCase(any()) } returns AutoVersionAuthScopeUseCase.Result.Success(authenticationScope) every { authenticationScope.login } returns loginUseCase every { authenticationScope.requestSecondFactorVerificationCode } returns requestSecondFactorCodeUseCase @@ -877,9 +873,10 @@ class LoginEmailViewModelTest { } fun arrange() = this to LoginEmailViewModel( + LoginNavArgs(loginPasswordPath = LoginPasswordPath(newServerConfig(1).links)), addAuthenticatedUserUseCase, clientScopeProviderFactory, - savedStateHandle, + savedInputStore, userDataStoreProvider, coreLogic, countdownTimer, diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt index b56397c214c..36fcdd4bff7 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt @@ -19,9 +19,7 @@ package com.wire.android.ui.authentication.login.sso import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.assertions.shouldBeEqualTo import com.wire.android.assertions.shouldBeInstanceOf import com.wire.android.assertions.shouldNotBeInstanceOf @@ -29,13 +27,13 @@ import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.SnapshotExtension import com.wire.android.config.TestDispatcherProvider -import com.wire.android.config.mockUri import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.ClientScopeProvider import com.wire.android.framework.TestClient import com.wire.android.framework.TestUser import com.wire.android.ui.authentication.login.LoginNavArgs import com.wire.android.ui.authentication.login.LoginPasswordPath +import com.wire.android.ui.authentication.login.LoginSavedInputStore import com.wire.android.ui.authentication.login.LoginState import com.wire.android.ui.authentication.login.SSOCodeAutoLogin import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState @@ -204,6 +202,49 @@ class LoginSSOViewModelTest { } } + @Test + fun `given auto login params, when handled, then sso code is prefilled without reading navigation args`() { + val expectedSSOCode = "wire-fd994b20-b9af-11ec-ae36-00163e9b33ca" + val (_, loginViewModel) = Arrangement().arrange() + + loginViewModel.handleSSOCodeAutoLogin( + ssoCode = expectedSSOCode, + autoInitiateLogin = false, + nomadServiceUrl = "https://nomad.example.com/service", + cookieLabel = "shared-device" + ) + + loginViewModel.ssoTextState.text.toString() shouldBeEqualTo expectedSSOCode + } + + @Test + fun `given auto login params with auto initiate, when handled, then login starts with cookie label`() = runTest { + val expectedSSOCode = "wire-fd994b20-b9af-11ec-ae36-00163e9b33ca" + val (arrangement, loginViewModel) = Arrangement() + .withValidateEmailReturning(false) + .withInitiateSSO(expectedSSOCode) + .arrange() + + loginViewModel.handleSSOCodeAutoLogin( + ssoCode = expectedSSOCode, + autoInitiateLogin = true, + nomadServiceUrl = "https://nomad.example.com/service", + cookieLabel = "shared-device" + ) + advanceUntilIdle() + + coVerify(exactly = 1) { + arrangement.ssoExtension.initiateSSO( + eq(SERVER_CONFIG.links), + eq(expectedSSOCode), + eq("shared-device"), + any(), + any(), + any() + ) + } + } + @Test fun `given sso code and button is clicked, when login returns InvalidCode error, then InvalidCodeError is passed`() = runTest { val expectedSSOCode = "wire-fd994b20-b9af-11ec-ae36-00163e9b33ca" @@ -1018,7 +1059,7 @@ class LoginSSOViewModelTest { private class Arrangement { @MockK - lateinit var savedStateHandle: SavedStateHandle + lateinit var savedInputStore: LoginSavedInputStore @MockK lateinit var ssoInitiateLoginUseCase: SSOInitiateLoginUseCase @@ -1076,15 +1117,10 @@ class LoginSSOViewModelTest { init { MockKAnnotations.init(this) - mockUri() - every { savedStateHandle.get(any()) } returns null - every { savedStateHandle.set(any(), any()) } returns Unit + every { savedInputStore.ssoCode } returns null + every { savedInputStore.ssoCode = any() } returns Unit every { clientScopeProviderFactory.create(any()).clientScope } returns clientScope every { clientScope.getOrRegister } returns getOrRegisterClientUseCase - every { savedStateHandle.navArgs() } returns LoginNavArgs( - loginPasswordPath = LoginPasswordPath(SERVER_CONFIG.links) - ) - coEvery { autoVersionAuthScopeUseCase(null) } returns AutoVersionAuthScopeUseCase.Result.Success( @@ -1179,28 +1215,41 @@ class LoginSSOViewModelTest { coEvery { doesValidSessionExistUseCase(any()) } returns DoesValidSessionExistResult.Success(valid) } + private var ssoCodeAutoLogin: SSOCodeAutoLogin? = null + fun withNomadAutoLogin(nomadServiceUrl: String) = apply { - every { savedStateHandle.navArgs() } returns LoginNavArgs( - loginPasswordPath = LoginPasswordPath(SERVER_CONFIG.links), - ssoCodeAutoLogin = SSOCodeAutoLogin( - ssoCode = "wire-sso-code", - nomadServiceUrl = nomadServiceUrl, - cookieLabel = "shared-device" - ) + ssoCodeAutoLogin = SSOCodeAutoLogin( + ssoCode = "wire-sso-code", + autoInitiateLogin = false, + nomadServiceUrl = nomadServiceUrl, + cookieLabel = "shared-device" ) } - fun arrange() = this to LoginSSOViewModel( - savedStateHandle = savedStateHandle, - addAuthenticatedUser = addAuthenticatedUserUseCase, - validateEmailUseCase = validateEmailUseCase, - coreLogic = coreLogic, - clientScopeProviderFactory = clientScopeProviderFactory, - userDataStoreProvider = userDataStoreProvider, - serverConfig = SERVER_CONFIG.links, - ssoExtension = ssoExtension, - dispatchers = TestDispatcherProvider(), - ) + fun arrange(): Pair { + val viewModel = LoginSSOViewModel( + loginNavArgs = LoginNavArgs(loginPasswordPath = LoginPasswordPath(SERVER_CONFIG.links)), + savedInputStore = savedInputStore, + addAuthenticatedUser = addAuthenticatedUserUseCase, + validateEmailUseCase = validateEmailUseCase, + coreLogic = coreLogic, + clientScopeProviderFactory = clientScopeProviderFactory, + userDataStoreProvider = userDataStoreProvider, + serverConfig = SERVER_CONFIG.links, + ssoExtension = ssoExtension, + sessionExceptionClassifier = LoginSSOSessionExceptionClassifier(), + dispatchers = TestDispatcherProvider(), + ) + ssoCodeAutoLogin?.let { + viewModel.handleSSOCodeAutoLogin( + ssoCode = it.ssoCode, + autoInitiateLogin = it.autoInitiateLogin, + nomadServiceUrl = it.nomadServiceUrl, + cookieLabel = it.cookieLabel, + ) + } + return this to viewModel + } } companion object { diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelTest.kt index 0ca7fdfa1ed..6e55415d420 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelTest.kt @@ -18,11 +18,8 @@ package com.wire.android.ui.authentication.welcome -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.config.mockUri -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.newServerConfig import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.session.DoesValidNomadAccountExistUseCase @@ -30,7 +27,6 @@ import com.wire.kalium.logic.feature.session.GetAllSessionsResult import com.wire.kalium.logic.feature.session.GetSessionsUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle @@ -41,12 +37,9 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class) +@ExtendWith(CoroutineTestExtension::class) class WelcomeViewModelTest { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var getSessions: GetSessionsUseCase @@ -54,20 +47,21 @@ class WelcomeViewModelTest { lateinit var doesValidNomadAccountExist: DoesValidNomadAccountExistUseCase private lateinit var welcomeViewModel: WelcomeViewModel + private lateinit var welcomeNavArgs: WelcomeNavArgs @BeforeEach fun setUp() { MockKAnnotations.init(this, relaxUnitFun = true) mockUri() val authServer = newServerConfig(1) - every { savedStateHandle.navArgs() } returns WelcomeNavArgs(authServer.links) + welcomeNavArgs = WelcomeNavArgs(authServer.links) coEvery { getSessions() } returns GetAllSessionsResult.Success(listOf()) coEvery { doesValidNomadAccountExist() } returns false } @Test fun `given no nomad account exists, when checking sessions, then nomadAccountBlocksLogin is false`() = runTest { - welcomeViewModel = WelcomeViewModel(savedStateHandle, getSessions, doesValidNomadAccountExist, ServerConfig.STAGING) + welcomeViewModel = WelcomeViewModel(welcomeNavArgs, getSessions, doesValidNomadAccountExist, ServerConfig.STAGING) advanceUntilIdle() assertEquals(false, welcomeViewModel.state.nomadAccountBlocksLogin) @@ -77,7 +71,7 @@ class WelcomeViewModelTest { fun `given nomad account exists, when checking sessions, then nomadAccountBlocksLogin is true`() = runTest { coEvery { doesValidNomadAccountExist() } returns true - welcomeViewModel = WelcomeViewModel(savedStateHandle, getSessions, doesValidNomadAccountExist, ServerConfig.STAGING) + welcomeViewModel = WelcomeViewModel(welcomeNavArgs, getSessions, doesValidNomadAccountExist, ServerConfig.STAGING) advanceUntilIdle() assertEquals(true, welcomeViewModel.state.nomadAccountBlocksLogin) diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt index c6b3441ec7f..d2b92f7d476 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt @@ -18,8 +18,6 @@ package com.wire.android.ui.calling -import android.view.Surface -import android.view.View import app.cash.turbine.test import com.wire.android.assertions.shouldBeEqualTo import com.wire.android.config.CoroutineTestExtension @@ -52,6 +50,7 @@ import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.util.PlatformRotation +import com.wire.kalium.logic.util.PlatformView import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -194,7 +193,7 @@ class SharedCallingViewModelTest { fun `given a call, when setVideoPreview is called, then set the video preview`() = runTest(dispatchers.main()) { val (arrangement, sharedCallingViewModel) = Arrangement().arrange() - sharedCallingViewModel.setVideoPreview(arrangement.view) + sharedCallingViewModel.setVideoPreview(arrangement.platformView) advanceUntilIdle() coVerify(exactly = 2) { arrangement.setVideoPreview(any(), any()) } @@ -231,12 +230,12 @@ class SharedCallingViewModelTest { val (arrangement, sharedCallingViewModel) = Arrangement().arrange() // when - sharedCallingViewModel.setUIRotation(Surface.ROTATION_90) + sharedCallingViewModel.setUIRotation(arrangement.platformRotation) advanceUntilIdle() // then coVerify(exactly = 1) { - arrangement.setUIRotationUseCase(eq(PlatformRotation(Surface.ROTATION_90))) + arrangement.setUIRotationUseCase(eq(arrangement.platformRotation)) } } @@ -281,7 +280,10 @@ class SharedCallingViewModelTest { lateinit var setUIRotationUseCase: SetUIRotationUseCase @MockK - lateinit var view: View + lateinit var platformRotation: PlatformRotation + + @MockK + lateinit var platformView: PlatformView @MockK lateinit var userTypeMapper: UserTypeMapper diff --git a/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt index 30d01263514..ad4277dead5 100644 --- a/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelTest.kt @@ -58,7 +58,7 @@ class ConnectionActionButtonViewModelTest { @Test fun `given success, when sending a connection request, then emit success message`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withSendConnectionRequest(SendConnectionRequestResult.Success) .arrange() @@ -77,7 +77,7 @@ class ConnectionActionButtonViewModelTest { @Test fun `given a failure, when sending a connection request, then emit failure message`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withSendConnectionRequest(SendConnectionRequestResult.Failure.GenericFailure(failure)) .arrange() @@ -96,7 +96,7 @@ class ConnectionActionButtonViewModelTest { @Test fun `given a federation denied failure, when sending a connection request, then emit proper failure message`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withSendConnectionRequest(SendConnectionRequestResult.Failure.FederationDenied) .arrange() @@ -115,7 +115,7 @@ class ConnectionActionButtonViewModelTest { @Test fun `given a legal hold failure, when sending a connection request, then edit the state properly`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withSendConnectionRequest(SendConnectionRequestResult.Failure.MissingLegalHoldConsent) .arrange() @@ -135,7 +135,7 @@ class ConnectionActionButtonViewModelTest { fun `given success, when ignoring a connection request, then calls onIgnoreSuccess`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withIgnoreConnectionRequest(IgnoreConnectionRequestUseCaseResult.Success) .arrange() @@ -157,7 +157,7 @@ class ConnectionActionButtonViewModelTest { fun `given failure, when ignoring a connection request, then emit error message`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withIgnoreConnectionRequest(IgnoreConnectionRequestUseCaseResult.Failure(failure)) .arrange() @@ -177,7 +177,7 @@ class ConnectionActionButtonViewModelTest { fun `given success, when canceling a connection request, then emit success message`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withCancelConnectionRequest(CancelConnectionRequestUseCaseResult.Success) .arrange() @@ -197,7 +197,7 @@ class ConnectionActionButtonViewModelTest { fun `given failure, when canceling a connection request, then emit failure message`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withCancelConnectionRequest(CancelConnectionRequestUseCaseResult.Failure(failure)) .arrange() @@ -217,7 +217,7 @@ class ConnectionActionButtonViewModelTest { fun `given success, when accepting a connection request, then emit success message`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withAcceptConnectionRequest(AcceptConnectionRequestUseCaseResult.Success) .arrange() @@ -237,7 +237,7 @@ class ConnectionActionButtonViewModelTest { fun `given failure, when accepting a connection request, then emit failure message`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withAcceptConnectionRequest(AcceptConnectionRequestUseCaseResult.Failure(failure)) .arrange() @@ -257,7 +257,7 @@ class ConnectionActionButtonViewModelTest { fun `given a conversationId, when trying to open the conversation, then returns a Success result with the conversation`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withGetOneToOneConversation(CreateConversationResult.Success(TestConversation.CONVERSATION)) .arrange() @@ -282,7 +282,7 @@ class ConnectionActionButtonViewModelTest { fun `given a conversationId, when trying to open the conversation and fails, then returns a Failure result and update error state`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withGetOneToOneConversation(CreateConversationResult.Failure(failure)) .arrange() @@ -305,7 +305,7 @@ class ConnectionActionButtonViewModelTest { fun `given a conversationId, when trying to open the conversation and fails with MissingKeyPackages, then call MissingKeyPackage()`() = runTest { // given - val (arrangement, viewModel) = ConnectionActionButtonHiltArrangement() + val (arrangement, viewModel) = ConnectionActionButtonArrangement() .withGetOneToOneConversation(CreateConversationResult.Failure(CoreFailure.MissingKeyPackages(setOf()))) .arrange() @@ -332,7 +332,7 @@ class ConnectionActionButtonViewModelTest { } } -internal class ConnectionActionButtonHiltArrangement { +internal class ConnectionActionButtonArrangement { @MockK lateinit var getOrCreateOneToOneConversation: GetOrCreateOneToOneConversationUseCase diff --git a/app/src/test/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModelTest.kt new file mode 100644 index 00000000000..4de6aa0311e --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModelTest.kt @@ -0,0 +1,163 @@ +/* + * 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.debug + +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.config.TestDispatcherProvider +import com.wire.android.ui.home.settings.backup.BackupAndRestoreState +import com.wire.android.ui.home.settings.backup.BackupCreationProgress +import com.wire.kalium.common.error.CoreFailure +import com.wire.kalium.logic.feature.backup.CreateBackupResult +import com.wire.kalium.logic.feature.backup.CreateObfuscatedCopyUseCase +import com.wire.kalium.util.DelicateKaliumApi +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import okio.IOException +import okio.Path +import okio.Path.Companion.toPath +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@OptIn(DelicateKaliumApi::class, ExperimentalCoroutinesApi::class) +@ExtendWith(CoroutineTestExtension::class) +class ExportObfuscatedCopyViewModelTest { + + @Test + fun givenCopyCreationSucceeds_whenCreatingObfuscatedCopy_thenFinishedStateAndLatestBackupAreSet() = runTest { + val backupPath = "backup-file-path".toPath() + val backupName = "backup-name.zip" + val (_, viewModel) = Arrangement() + .withSuccessfulCreation(backupPath, backupName) + .arrange() + + viewModel.createObfuscatedCopy() + advanceUntilIdle() + + assertEquals(BackupCreationProgress.Finished(backupName), viewModel.state.backupCreationProgress) + assertEquals( + BackupAndRestoreState.CreatedBackup(backupPath, backupName, false), + viewModel.latestCreatedBackup + ) + } + + @Test + fun givenCopyCreationFails_whenCreatingObfuscatedCopy_thenFailedStateIsSet() = runTest { + val (arrangement, viewModel) = Arrangement() + .withFailedCreation() + .arrange() + + viewModel.createObfuscatedCopy() + advanceUntilIdle() + + assertEquals(BackupCreationProgress.Failed, viewModel.state.backupCreationProgress) + assertNull(viewModel.latestCreatedBackup) + coVerify(exactly = 1) { arrangement.createUnencryptedCopy(null) } + } + + @Test + fun givenACreatedCopy_whenSharingIt_thenGatewaySharesCopyAndStateIsReset() = runTest { + val createdCopy = BackupAndRestoreState.CreatedBackup("backup-file-path".toPath(), "backup-name.zip", false) + val (arrangement, viewModel) = Arrangement() + .withPreviouslyCreatedCopy(createdCopy) + .arrange() + + viewModel.shareCopy() + advanceUntilIdle() + + assertEquals(listOf(createdCopy.path to createdCopy.assetName), arrangement.fileGateway.sharedCopies) + assertEquals(BackupCreationProgress.InProgress(), viewModel.state.backupCreationProgress) + } + + @Test + fun givenACreatedCopy_whenSavingIt_thenGatewaySavesCopyAndStateIsReset() = runTest { + val createdCopy = BackupAndRestoreState.CreatedBackup("backup-file-path".toPath(), "backup-name.zip", false) + val destinationUri = "content://backup-destination" + val (arrangement, viewModel) = Arrangement() + .withPreviouslyCreatedCopy(createdCopy) + .arrange() + + viewModel.saveCopy(destinationUri) + advanceUntilIdle() + + assertEquals(listOf(createdCopy.path to destinationUri), arrangement.fileGateway.savedCopies) + assertEquals(BackupCreationProgress.InProgress(), viewModel.state.backupCreationProgress) + } + + private class Arrangement { + + @MockK + lateinit var createUnencryptedCopy: CreateObfuscatedCopyUseCase + + val fileGateway = FakeExportObfuscatedCopyFileGateway() + + private val viewModel: ExportObfuscatedCopyViewModelImpl + + init { + MockKAnnotations.init(this) + coEvery { createUnencryptedCopy(null) } returns CreateBackupResult.Success( + "backup-file-path".toPath(), + "backup-name.zip" + ) + viewModel = ExportObfuscatedCopyViewModelImpl( + createUnencryptedCopy = createUnencryptedCopy, + dispatcher = TestDispatcherProvider(), + fileGateway = fileGateway, + ) + } + + fun withSuccessfulCreation(path: Path, name: String) = apply { + coEvery { createUnencryptedCopy(null) } returns CreateBackupResult.Success(path, name) + } + + fun withFailedCreation() = apply { + coEvery { createUnencryptedCopy(null) } returns CreateBackupResult.Failure( + CoreFailure.Unknown(IOException("Some db error")) + ) + } + + fun withPreviouslyCreatedCopy(createdCopy: BackupAndRestoreState.CreatedBackup) = apply { + viewModel.latestCreatedBackup = createdCopy + viewModel.state = viewModel.state.copy( + backupCreationProgress = BackupCreationProgress.Finished(createdCopy.assetName) + ) + } + + fun arrange() = this to viewModel + } + + private class FakeExportObfuscatedCopyFileGateway : ExportObfuscatedCopyFileGateway { + val sharedCopies = mutableListOf>() + val savedCopies = mutableListOf>() + + override suspend fun shareCopy(path: Path, assetName: String?) { + sharedCopies += path to assetName + } + + override suspend fun saveCopy(path: Path, destinationUri: String) { + savedCopies += path to destinationUri + } + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModelTest.kt index bd74eba3005..7e964eb5072 100644 --- a/app/src/test/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModelTest.kt @@ -19,11 +19,8 @@ package com.wire.android.ui.debug.conversation -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.framework.TestConversation import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.data.conversation.Conversation @@ -38,7 +35,6 @@ import com.wire.kalium.logic.feature.debug.GetConversationEpochFromCCUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle @@ -48,7 +44,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) class DebugConversationViewModelTest { @Test @@ -95,9 +90,6 @@ class DebugConversationViewModelTest { private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var observeConversationDetailsUseCase: ObserveConversationDetailsUseCase @@ -117,9 +109,6 @@ private class Arrangement { init { MockKAnnotations.init(this, relaxUnitFun = true) - every { - savedStateHandle.navArgs() - } returns DebugConversationScreenNavArgs(conversationId) coEvery { observeConversationDetailsUseCase(any()) } returns flowOf() coEvery { getConversationEpochFromCCUseCase(any()) } returns GetConversationEpochFromCCResult.Failure.NotMlsConversation } @@ -144,7 +133,7 @@ private class Arrangement { fetchConversation = fetchConversationUseCase, feedConversation = debugFeedConversationUseCase, getConversationEpochFromCC = getConversationEpochFromCCUseCase, - savedStateHandle = savedStateHandle, + args = DebugConversationScreenNavArgs(conversationId), ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt index 0137b8a4a22..436034b83b4 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.home -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension import com.wire.android.datastore.GlobalDataStore @@ -143,9 +142,6 @@ class HomeViewModelTest { internal class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var globalDataStore: GlobalDataStore @@ -169,7 +165,6 @@ class HomeViewModelTest { private val viewModel by lazy { HomeViewModel( - savedStateHandle = savedStateHandle, dataStore = dataStore, observeSelf = observeSelfUser, needsToRegisterClient = needsToRegisterClient, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelTest.kt index 4ebe3a3a1b2..3f5f6795628 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelTest.kt @@ -17,17 +17,13 @@ */ package com.wire.android.ui.home.conversations -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.ui.home.conversations.model.CompositeMessageArgs -import com.wire.android.config.NavigationTestExtension -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -36,7 +32,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) class CompositeMessageViewModelTest { @Test @@ -86,17 +81,13 @@ class CompositeMessageViewModelTest { @MockK lateinit var sendButtonActionMessage: SendButtonActionMessageUseCase - @MockK - lateinit var savedStateHandle: SavedStateHandle - - private val scopedArgs = CompositeMessageArgs(MESSAGE_ID) + private val scopedArgs = CompositeMessageArgs(CONVERSATION_ID, MESSAGE_ID) init { MockKAnnotations.init(this) - every { savedStateHandle.navArgs() } returns ConversationNavArgs(CONVERSATION_ID) } - private val viewModel = CompositeMessageViewModelImpl(sendButtonActionMessage, savedStateHandle, scopedArgs) + private val viewModel = CompositeMessageViewModelImpl(sendButtonActionMessage, scopedArgs) fun withButtonActionMessage( result: SendButtonActionMessageUseCase.Result diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModelTest.kt index 195d3c34db4..494f86cc4d8 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModelTest.kt @@ -17,14 +17,12 @@ */ package com.wire.android.ui.home.conversations.attachment -import androidx.lifecycle.SavedStateHandle -import com.ramcosta.composedestinations.generated.app.navargs.toSavedStateHandle import com.wire.android.config.CoroutineTestExtension +import com.wire.android.ui.common.attachmentdraft.model.AttachmentDraftUi import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.MessageSharedState import com.wire.android.ui.home.conversations.model.AssetBundle -import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase -import com.wire.android.util.FileManager +import com.wire.android.ui.sharing.ImportedMediaAsset import com.wire.android.util.GetMediaMetadataUseCase import com.wire.kalium.cells.domain.CellUploadManager import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCase @@ -34,11 +32,13 @@ import com.wire.kalium.cells.domain.usecase.RetryAttachmentUploadUseCase import com.wire.kalium.common.functional.right import com.wire.kalium.logic.data.asset.AttachmentType import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.AssetContent import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every +import io.mockk.verify import io.mockk.impl.annotations.MockK -import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.runCurrent @@ -60,7 +60,7 @@ class MessageAttachmentsViewModelTest { .withAddAttachmentSuccess() .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Hidden) coVerify(exactly = 1) { arrangement.addAttachment(any(), any(), any(), any(), any(), any()) } @@ -72,7 +72,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess(".hidden.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -83,7 +83,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess("bad/name.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -94,7 +94,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess("bad\\name.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -105,7 +105,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess("bad\"name.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -116,7 +116,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess(".hidden.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) coVerify(exactly = 0) { arrangement.addAttachment(any(), any(), any(), any(), any(), any()) } } @@ -127,7 +127,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess(".hidden.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) val state = viewModel.incompatibleFileNameDialogState as IncompatibleFileNameDialogState.Visible assertEquals("hidden.txt", state.sanitizedFileName) @@ -139,7 +139,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess("bad/slash\\name.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) val state = viewModel.incompatibleFileNameDialogState as IncompatibleFileNameDialogState.Visible assertEquals("bad_slash_name.txt", state.sanitizedFileName) @@ -151,7 +151,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess("...") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) val state = viewModel.incompatibleFileNameDialogState as IncompatibleFileNameDialogState.Visible assertEquals("file", state.sanitizedFileName) @@ -163,7 +163,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess(".") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -174,7 +174,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess(".") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) val state = viewModel.incompatibleFileNameDialogState as IncompatibleFileNameDialogState.Visible assertEquals("file", state.sanitizedFileName) @@ -187,7 +187,7 @@ class MessageAttachmentsViewModelTest { .withAddAttachmentSuccess() .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) viewModel.onReplaceFileNameAutomatically() coVerify(exactly = 1) { @@ -209,7 +209,7 @@ class MessageAttachmentsViewModelTest { .withAddAttachmentSuccess() .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) viewModel.onReplaceFileNameAutomatically() assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Hidden) @@ -221,7 +221,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess(".hidden.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) viewModel.onDismissIncompatibleFileNameDialog() assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Hidden) @@ -234,7 +234,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess(".first.txt", ".second.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk(), mockk())) + viewModel.onFilesSelected(listOf("uri-1", "uri-2")) val state = viewModel.incompatibleFileNameDialogState as IncompatibleFileNameDialogState.Visible assertEquals("first.txt", state.sanitizedFileName) @@ -247,7 +247,7 @@ class MessageAttachmentsViewModelTest { .withAddAttachmentSuccess() .arrange() - viewModel.onFilesSelected(listOf(mockk(), mockk())) + viewModel.onFilesSelected(listOf("uri-1", "uri-2")) viewModel.onReplaceFileNameAutomatically() val state = viewModel.incompatibleFileNameDialogState as IncompatibleFileNameDialogState.Visible @@ -261,7 +261,7 @@ class MessageAttachmentsViewModelTest { .withAddAttachmentSuccess() .arrange() - viewModel.onFilesSelected(listOf(mockk(), mockk())) + viewModel.onFilesSelected(listOf("uri-1", "uri-2")) viewModel.onReplaceFileNameAutomatically() viewModel.onReplaceFileNameAutomatically() @@ -274,7 +274,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess(".first.txt", ".second.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk(), mockk())) + viewModel.onFilesSelected(listOf("uri-1", "uri-2")) viewModel.onDismissIncompatibleFileNameDialog() val state = viewModel.incompatibleFileNameDialogState as IncompatibleFileNameDialogState.Visible @@ -288,7 +288,7 @@ class MessageAttachmentsViewModelTest { .withAddAttachmentSuccess() .arrange() - viewModel.onFilesSelected(listOf(mockk(), mockk())) + viewModel.onFilesSelected(listOf("uri-1", "uri-2")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -315,6 +315,35 @@ class MessageAttachmentsViewModelTest { coVerify(exactly = 1) { arrangement.addAttachment(any(), eq("clean.pdf"), any(), any(), any(), any()) } } + @Test + fun givenUploadedAttachment_whenAttachmentClicked_thenFileGatewayOpensIt() = runTest { + val (arrangement, viewModel) = Arrangement().arrange() + val attachment = testAttachment(uploadError = false) + + viewModel.onAttachmentClicked(attachment) + + verify(exactly = 1) { + arrangement.fileGateway.open( + localFilePath = attachment.localFilePath, + fileName = attachment.fileName, + onError = any() + ) + } + } + + @Test + fun givenFailedAttachmentFileExists_whenAttachmentClicked_thenRetryOptionIsShown() = runTest { + val (_, viewModel) = Arrangement() + .withAttachmentFileExists(true) + .arrange() + val attachment = testAttachment(uploadError = true) + + viewModel.onAttachmentClicked(attachment) + + val state = viewModel.failedAttachmentDialogState as FailedAttachmentDialogState.Visible + assertTrue(state.showRetryOption) + } + private fun testBundle(fileName: String) = AssetBundle( key = "key", mimeType = "text/plain", @@ -324,16 +353,21 @@ class MessageAttachmentsViewModelTest { assetType = AttachmentType.GENERIC_FILE, ) + private fun testAttachment(uploadError: Boolean) = AttachmentDraftUi( + uuid = "uuid", + fileName = "file.txt", + localFilePath = "/tmp/file.txt", + uploadError = uploadError, + ) + private class Arrangement { - // Use the generated toSavedStateHandle() extension which correctly serializes via - // qualifiedIDNavType (QualifiedID uses @SerialName("id") for the value field, not "value"). - private val savedStateHandle: SavedStateHandle = ConversationNavArgs( + private val conversationNavArgs = ConversationNavArgs( conversationId = ConversationId("conv-value", "conv-domain") - ).toSavedStateHandle() + ) @MockK - lateinit var handleUriAsset: HandleUriAssetUseCase + lateinit var assetImporter: MessageAttachmentAssetImporter @MockK lateinit var observeAttachments: ObserveAttachmentDraftsUseCase @@ -351,7 +385,7 @@ class MessageAttachmentsViewModelTest { lateinit var uploadManager: CellUploadManager @MockK - lateinit var fileManager: FileManager + lateinit var fileGateway: MessageAttachmentFileGateway @MockK lateinit var getMediaMetadata: GetMediaMetadataUseCase @@ -368,6 +402,11 @@ class MessageAttachmentsViewModelTest { coEvery { observeAttachments(any()) } returns MutableSharedFlow() coEvery { uploadManager.getUploadInfo(any()) } returns null coEvery { getMediaMetadata(any(), any()) } returns null + every { fileGateway.exists(any()) } returns false + every { fileGateway.audioMetadata(any(), any(), any()) } returns AssetContent.AssetMetadata.Audio( + durationMs = 0L, + normalizedLoudness = null, + ) isInitialized = true } } @@ -375,9 +414,9 @@ class MessageAttachmentsViewModelTest { fun withHandleUriAssetSuccess(vararg fileNames: String) = apply { initializeMocks() uriAssetQueue.addAll(fileNames) - coEvery { handleUriAsset.invoke(any(), any()) } answers { + coEvery { assetImporter.importAsset(any()) } answers { val name = uriAssetQueue.removeFirstOrNull() ?: "file.txt" - HandleUriAssetUseCase.Result.Success( + ImportedMediaAsset( assetBundle = AssetBundle( key = "key", mimeType = "text/plain", @@ -385,7 +424,8 @@ class MessageAttachmentsViewModelTest { dataSize = 100L, fileName = name, assetType = AttachmentType.GENERIC_FILE, - ) + ), + assetSizeExceeded = null, ) } } @@ -395,17 +435,22 @@ class MessageAttachmentsViewModelTest { coEvery { addAttachment(any(), any(), any(), any(), any(), any()) } returns Unit.right() } + fun withAttachmentFileExists(exists: Boolean) = apply { + initializeMocks() + every { fileGateway.exists(any()) } returns exists + } + fun arrange(): Pair { initializeMocks() val viewModel = MessageAttachmentsViewModel( - savedStateHandle = savedStateHandle, - handleUriAsset = handleUriAsset, + conversationNavArgs = conversationNavArgs, + assetImporter = assetImporter, observeAttachments = observeAttachments, addAttachment = addAttachment, removeAttachment = removeAttachment, retryUpload = retryUpload, uploadManager = uploadManager, - fileManager = fileManager, + fileGateway = fileGateway, sharedState = sharedState, getMediaMetadata = getMediaMetadata, ) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModelTest.kt index fbe77a92dd7..e368af741f7 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModelTest.kt @@ -18,15 +18,12 @@ package com.wire.android.ui.home.conversations.banner -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.config.mockUri import com.wire.android.framework.TestConversationDetails import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.banner.usecase.ObserveConversationMembersByTypesUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.data.user.type.UserTypeInfo @@ -35,7 +32,6 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseC import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -47,7 +43,6 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) class ConversationBannerViewModelTest { @Test @@ -129,9 +124,6 @@ class ConversationBannerViewModelTest { private class Arrangement { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var observeConversationMembersByTypesUseCase: ObserveConversationMembersByTypesUseCase @@ -143,7 +135,7 @@ private class Arrangement { private val viewModel by lazy { ConversationBannerViewModel( - savedStateHandle, + ConversationNavArgs(conversationId = conversationId), observeConversationMembersByTypesUseCase, observeConversationDetailsUseCase, notifyConversationIsOpenUseCase @@ -155,7 +147,6 @@ private class Arrangement { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) mockUri() - every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId = conversationId) // Default empty values coEvery { observeConversationMembersByTypesUseCase(any()) } returns flowOf() coEvery { notifyConversationIsOpenUseCase(any()) } returns Unit 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 15dba6f3805..29a1effe1d5 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 @@ -17,14 +17,11 @@ */ package com.wire.android.ui.home.conversations.call -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType @@ -44,7 +41,6 @@ import com.wire.kalium.logic.sync.ObserveSyncStateUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf @@ -54,7 +50,7 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class) +@ExtendWith(CoroutineTestExtension::class) class ConversationCallViewModelTest { @Test @@ -154,9 +150,6 @@ class ConversationCallViewModelTest { } private class Arrangement { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK private lateinit var observeOngoingCalls: ObserveOngoingCallsUseCase @@ -201,7 +194,6 @@ class ConversationCallViewModelTest { } suspend fun withDefaultAnswers() = apply { - every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId = conversationId) coEvery { observeEstablishedCalls.invoke() } returns emptyFlow() coEvery { observeOngoingCalls.invoke() } returns emptyFlow() coEvery { observeConversationDetails(any()) } returns flowOf() @@ -233,7 +225,7 @@ class ConversationCallViewModelTest { } fun arrange(): Pair = this to ConversationCallViewModel( - savedStateHandle = savedStateHandle, + conversationNavArgs = ConversationNavArgs(conversationId = conversationId), observeOngoingCalls = observeOngoingCalls, observeEstablishedCalls = observeEstablishedCalls, answerCall = joinCall, 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..490213155d6 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 @@ -18,12 +18,8 @@ package com.wire.android.ui.home.conversations.composer -import android.net.Uri -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.TestDispatcherProvider -import com.wire.android.config.mockUri import com.wire.android.datastore.GlobalDataStore -import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.framework.TestConversation import com.wire.android.framework.TestUser import com.wire.android.mapper.ContactMapper @@ -37,8 +33,6 @@ import com.wire.android.ui.home.conversations.model.MessageStatus import com.wire.android.ui.home.conversations.model.MessageTime import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.model.UIMessageContent -import com.ramcosta.composedestinations.generated.app.navArgs -import com.wire.android.util.FileManager import com.wire.android.util.ui.UIText import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.data.auth.AccountInfo @@ -87,16 +81,12 @@ internal class MessageComposerViewModelArrangement { init { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) - mockUri() - every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId = conversationId) // Default empty values coEvery { isFileSharingEnabledUseCase() } returns FileSharingStatus(FileSharingStatus.Value.EnabledAll, null) coEvery { observeOngoingCallsUseCase() } returns flowOf(listOf()) coEvery { observeEstablishedCallsUseCase() } returns flowOf(listOf()) coEvery { observeSyncState() } returns flowOf(SyncState.Live) - coEvery { fileManager.getTempWritableVideoUri(any(), any()) } returns Uri.parse("video.mp4") - coEvery { fileManager.getTempWritableImageUri(any(), any()) } returns Uri.parse("image.jpg") coEvery { currentSessionFlowUseCase() } returns flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID))) @@ -105,8 +95,7 @@ internal class MessageComposerViewModelArrangement { coEvery { markConversationAsReadLocallyUseCase(any(), any()) } returns MarkConversationAsReadResult.Success(false) } - @MockK - private lateinit var savedStateHandle: SavedStateHandle + private val conversationNavArgs = ConversationNavArgs(conversationId = conversationId) @MockK lateinit var isFileSharingEnabledUseCase: IsFileSharingEnabledUseCase @@ -144,9 +133,6 @@ internal class MessageComposerViewModelArrangement { @MockK lateinit var sendTypingEvent: SendTypingEventUseCase - @MockK - lateinit var fileManager: FileManager - @MockK lateinit var currentSessionFlowUseCase: CurrentSessionFlowUseCase @@ -156,11 +142,11 @@ internal class MessageComposerViewModelArrangement { @MockK lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase - private val fakeKaliumFileSystem = FakeKaliumFileSystem() + val tempWritableAttachmentUriProvider = FakeTempWritableAttachmentUriProvider() private val viewModel by lazy { MessageComposerViewModel( - savedStateHandle = savedStateHandle, + conversationNavArgs = conversationNavArgs, dispatchers = TestDispatcherProvider(), isFileSharingEnabled = isFileSharingEnabledUseCase, updateConversationReadDate = updateConversationReadDateUseCase, @@ -171,8 +157,7 @@ internal class MessageComposerViewModelArrangement { enqueueMessageSelfDeletion = enqueueMessageSelfDeletionUseCase, persistNewSelfDeletingStatus = persistSelfDeletionStatus, sendTypingEvent = sendTypingEvent, - kaliumFileSystem = fakeKaliumFileSystem, - fileManager = fileManager, + tempWritableAttachmentUriProvider = tempWritableAttachmentUriProvider, currentSessionFlowUseCase = currentSessionFlowUseCase, observeEstablishedCalls = observeEstablishedCalls, globalDataStore = globalDataStore, @@ -198,6 +183,25 @@ internal class MessageComposerViewModelArrangement { } fun arrange() = this to viewModel + + class FakeTempWritableAttachmentUriProvider : TempWritableAttachmentUriProvider { + var tempWritableVideoUri = "video.mp4" + var tempWritableImageUri = "image.jpg" + var videoUriCalls = 0 + private set + var imageUriCalls = 0 + private set + + override suspend fun getTempWritableVideoUri(): String { + videoUriCalls++ + return tempWritableVideoUri + } + + override suspend fun getTempWritableImageUri(): String { + imageUriCalls++ + return tempWritableImageUri + } + } } internal fun withMockConversationDetailsOneOnOne( diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt index a65c98da7ae..475bc756ad8 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelTest.kt @@ -95,6 +95,21 @@ class MessageComposerViewModelTest { assertTrue(!viewModel.messageComposerViewState.value.enterToSend) } + @Test + fun `given temp attachment uris, when init, then expose provider values`() = runTest { + // given + val (arrangement, viewModel) = MessageComposerViewModelArrangement() + .withSuccessfulViewModelInit() + .arrange() + advanceUntilIdle() + + // then + assertEquals("image.jpg", viewModel.tempWritableImageUri) + assertEquals("video.mp4", viewModel.tempWritableVideoUri) + assertEquals(1, arrangement.tempWritableAttachmentUriProvider.imageUriCalls) + assertEquals(1, arrangement.tempWritableAttachmentUriProvider.videoUriCalls) + } + @Test fun `given messages were viewed, when conversation is closed, then mark as read with last viewed timestamp`() = runTest { // given 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..fce2b383c06 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 @@ -19,7 +19,6 @@ package com.wire.android.ui.home.conversations.details -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider @@ -28,12 +27,10 @@ import com.wire.android.framework.TestTeam import com.wire.android.framework.TestUser import com.wire.android.mapper.testUIParticipant import com.wire.android.ui.home.conversations.details.options.GroupConversationOptionsState -import com.wire.android.ui.home.conversations.details.participants.GroupConversationAllParticipantsNavArgs import com.wire.android.ui.home.conversations.details.participants.model.ConversationParticipantsData import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase import com.wire.android.ui.home.newconversation.channelaccess.ChannelAccessType import com.wire.android.ui.home.newconversation.channelaccess.ChannelAddPermissionType -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationDetails.Group.Channel.ChannelAccess @@ -66,7 +63,6 @@ import com.wire.kalium.logic.feature.user.IsMLSEnabledUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow @@ -744,9 +740,6 @@ class GroupDetailsViewModelTest { internal class GroupConversationDetailsViewModelArrangement { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var observeConversationDetails: ObserveConversationDetailsUseCase @@ -790,12 +783,12 @@ internal class GroupConversationDetailsViewModelArrangement { private val viewModel by lazy { GroupConversationDetailsViewModel( + groupConversationDetailsNavArgs = GroupConversationDetailsNavArgs(conversationId = conversationId), dispatcher = TestDispatcherProvider(), observeConversationDetails = observeConversationDetails, observeConversationMembers = observeParticipantsForConversationUseCase, observeSelfUserWithTeam = observeSelfUserWithTeam, observeIsAppsAllowedForUsage = observeIsAppsAllowedForUsage, - savedStateHandle = savedStateHandle, updateConversationReceiptMode = updateConversationReceiptMode, isMLSEnabled = isMLSEnabledUseCase, observeSelfDeletionTimerSettingsForConversation = observeSelfDeletionTimerSettingsForConversation, @@ -810,15 +803,6 @@ internal class GroupConversationDetailsViewModelArrangement { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) - every { - savedStateHandle.navArgs() - } returns GroupConversationAllParticipantsNavArgs( - conversationId = conversationId - ) - every { savedStateHandle.navArgs() } returns GroupConversationDetailsNavArgs( - conversationId = conversationId - ) - // Default empty values coEvery { observeConversationDetails(any()) } returns flowOf() updateSelfUserWithTeamFlow() diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreatePasswordGuestLinkViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreatePasswordGuestLinkViewModelTest.kt index 3ba3eb04e9e..57c9ce7211f 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreatePasswordGuestLinkViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/CreatePasswordGuestLinkViewModelTest.kt @@ -18,12 +18,9 @@ package com.wire.android.ui.home.conversations.details.editguestaccess import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.wire.android.config.NavigationTestExtension import com.wire.android.ui.home.conversations.details.editguestaccess.createPasswordProtectedGuestLink.CreatePasswordGuestLinkNavArgs import com.wire.android.ui.home.conversations.details.editguestaccess.createPasswordProtectedGuestLink.CreatePasswordGuestLinkViewModel -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.common.error.NetworkFailure import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.auth.ValidatePasswordResult @@ -52,11 +49,6 @@ import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith( - NavigationTestExtension::class -) class CreatePasswordGuestLinkViewModelTest { private val dispatcher: TestDispatcher = StandardTestDispatcher() @@ -273,9 +265,6 @@ class CreatePasswordGuestLinkViewModelTest { private class Arrangement(private val dispatcher: TestDispatcher) { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var generateGuestRoomLink: GenerateGuestRoomLinkUseCase @@ -287,11 +276,6 @@ class CreatePasswordGuestLinkViewModelTest { init { MockKAnnotations.init(this) - every { - savedStateHandle.navArgs() - } returns CreatePasswordGuestLinkNavArgs( - conversationId = CONVERSATION_ID - ) } fun withPasswordValidation(result: Boolean) = apply { @@ -328,10 +312,12 @@ class CreatePasswordGuestLinkViewModelTest { private val viewModel: CreatePasswordGuestLinkViewModel by lazy { CreatePasswordGuestLinkViewModel( + createPasswordGuestLinkNavArgs = CreatePasswordGuestLinkNavArgs( + conversationId = CONVERSATION_ID + ), generateGuestRoomLink = generateGuestRoomLink, validatePassword = validatePassword, generatePassword = generateRandomPassword, - savedStateHandle = savedStateHandle ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt index b11e80a669c..0f78ed688b9 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelTest.kt @@ -20,16 +20,13 @@ package com.wire.android.ui.home.conversations.details.editguestaccess -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.framework.TestConversation import com.wire.android.framework.TestConversationDetails import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.details.participants.model.ConversationParticipantsData import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModelTest import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.common.error.NetworkFailure @@ -50,7 +47,6 @@ import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeat import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -64,7 +60,7 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class) +@ExtendWith(CoroutineTestExtension::class) class EditGuestAccessViewModelTest { private val dispatcher = TestDispatcherProvider() @@ -256,9 +252,6 @@ class EditGuestAccessViewModelTest { } private class Arrangement(dispatcherProvider: TestDispatcherProvider) { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var observeConversationDetails: ObserveConversationDetailsUseCase @@ -294,7 +287,14 @@ class EditGuestAccessViewModelTest { val editGuestAccessViewModel: EditGuestAccessViewModel by lazy { EditGuestAccessViewModel( - savedStateHandle = savedStateHandle, + editGuestAccessNavArgs = EditGuestAccessNavArgs( + conversationId = OtherUserProfileScreenViewModelTest.CONVERSATION_ID, + editGuessAccessParams = EditGuestAccessParams( + isGuestAccessAllowed = true, + isServicesAllowed = true, + isUpdatingGuestAccessAllowed = true + ) + ), observeConversationDetails = observeConversationDetails, observeConversationMembers = observeConversationMembers, updateConversationAccessRole = updateConversationAccessRole, @@ -312,14 +312,6 @@ class EditGuestAccessViewModelTest { init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.navArgs() } returns EditGuestAccessNavArgs( - conversationId = OtherUserProfileScreenViewModelTest.CONVERSATION_ID, - editGuessAccessParams = EditGuestAccessParams( - isGuestAccessAllowed = true, - isServicesAllowed = true, - isUpdatingGuestAccessAllowed = true - ) - ) coEvery { observeConversationDetails(any()) } returns flowOf() coEvery { observeConversationMembers(any()) } returns flowOf() coEvery { observeGuestRoomLink(any()) } returns flowOf() diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModelTest.kt index 48088ba1152..b2c7738cb50 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModelTest.kt @@ -17,9 +17,7 @@ */ package com.wire.android.ui.home.conversations.details.editselfdeletingmessages -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.framework.TestConversation import com.wire.android.framework.TestConversationDetails @@ -27,7 +25,6 @@ import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.details.participants.model.ConversationParticipantsData import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.messagetimer.UpdateMessageTimerUseCase @@ -35,7 +32,6 @@ import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTim import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -48,7 +44,6 @@ import kotlin.time.Duration.Companion.minutes @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) class EditSelfDeletingMessagesViewModelTest { @Test @@ -87,9 +82,6 @@ class EditSelfDeletingMessagesViewModelTest { private class Arrangement { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK private lateinit var observerConversationMembers: ObserveParticipantsForConversationUseCase @@ -107,7 +99,9 @@ class EditSelfDeletingMessagesViewModelTest { private val viewModel by lazy { EditSelfDeletingMessagesViewModel( - savedStateHandle = savedStateHandle, + editSelfDeletingMessagesNavArgs = EditSelfDeletingMessagesNavArgs( + conversationId = TestConversation.ID + ), dispatcher = TestDispatcherProvider(), observeConversationMembers = observerConversationMembers, observeSelfDeletionTimerSettingsForConversation = observeSelfDeletionTimerSettingsForConversation, @@ -119,10 +113,6 @@ class EditSelfDeletingMessagesViewModelTest { init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.navArgs() } returns EditSelfDeletingMessagesNavArgs( - conversationId = TestConversation.ID - ) - coEvery { selfUser() } returns flowOf(TestUser.SELF_USER) coEvery { conversationDetails(any()) } returns flowOf( ObserveConversationDetailsUseCase.Result.Success(TestConversationDetails.GROUP) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupParticipantsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupParticipantsViewModelTest.kt index 9ad036f133c..4e840d2b419 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupParticipantsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupParticipantsViewModelTest.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home.conversations.details.participants -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.mockUri @@ -26,13 +25,11 @@ import com.wire.android.mapper.testUIParticipant import com.wire.android.ui.home.conversations.details.participants.model.ConversationParticipantsData import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -74,9 +71,6 @@ class GroupParticipantsViewModelTest { internal class Arrangement { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK private lateinit var refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase @@ -89,9 +83,6 @@ internal class Arrangement { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) mockUri() - every { - savedStateHandle.navArgs() - } returns GroupConversationAllParticipantsNavArgs(conversationId = conversationId) // Default empty values coEvery { observeParticipantsForConversationUseCase(any(), any()) } returns flowOf() } @@ -110,7 +101,7 @@ internal class Arrangement { fun arrange(maxNumberOfItems: Int = -1): Pair = this to object : GroupConversationParticipantsViewModel( - savedStateHandle, + conversationId, observeParticipantsForConversationUseCase, refreshUsersWithoutMetadata, ) { 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..3e92f8a80c8 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 @@ -17,7 +17,6 @@ */ package com.wire.android.ui.home.conversations.details.updateappsaccess -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider @@ -26,7 +25,6 @@ import com.wire.android.framework.TestConversationDetails import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.details.participants.model.ConversationParticipantsData import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.MutedConversationStatus @@ -45,7 +43,6 @@ import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow @@ -395,9 +392,6 @@ class UpdateAppsAccessViewModelTest { internal class UpdateAppsAccessViewModelArrangement { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var observeConversationDetails: ObserveConversationDetailsUseCase @@ -413,6 +407,17 @@ internal class UpdateAppsAccessViewModelArrangement { @MockK lateinit var observeSelfUser: ObserveSelfUserUseCase + val conversationId = ConversationId("some-dummy-value", "dummyDomain") + + private var navArgs = UpdateAppsAccessNavArgs( + conversationId = conversationId, + updateAppsAccessParams = UpdateAppsAccessParams( + isGuestAllowed = true, + isAppsAllowed = true, + shouldUseNewAppsUi = true + ) + ) + private val conversationDetailsFlow = MutableSharedFlow(replay = Int.MAX_VALUE) private val observeParticipantsForConversationFlow = @@ -420,31 +425,20 @@ internal class UpdateAppsAccessViewModelArrangement { private val viewModel by lazy { UpdateAppsAccessViewModel( + updateAppsAccessNavArgs = navArgs, dispatcher = TestDispatcherProvider(), observeConversationDetails = observeConversationDetails, observeConversationMembers = observeParticipantsForConversationUseCase, changeAccessForAppsInConversation = changeAccessForAppsInConversationUseCase, observeIsAppsAllowedForUsage = observeIsAppsAllowedForUsage, selfUser = observeSelfUser, - savedStateHandle = savedStateHandle ) } - val conversationId = ConversationId("some-dummy-value", "dummyDomain") - init { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.navArgs() } returns UpdateAppsAccessNavArgs( - conversationId = conversationId, - updateAppsAccessParams = UpdateAppsAccessParams( - isGuestAllowed = true, - isAppsAllowed = true, - shouldUseNewAppsUi = true - ) - ) - // Default empty values coEvery { observeConversationDetails(any()) } returns flowOf() coEvery { observeParticipantsForConversationUseCase(any()) } returns flowOf() @@ -453,7 +447,7 @@ internal class UpdateAppsAccessViewModelArrangement { } fun withGuestDisabledNavArgs() = apply { - every { savedStateHandle.navArgs() } returns UpdateAppsAccessNavArgs( + navArgs = UpdateAppsAccessNavArgs( conversationId = conversationId, updateAppsAccessParams = UpdateAppsAccessParams( isGuestAllowed = false, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModelTest.kt index 50441450beb..615c8854e62 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModelTest.kt @@ -17,19 +17,14 @@ */ package com.wire.android.ui.home.conversations.details.updatechannelaccess -import androidx.lifecycle.SavedStateHandle import com.wire.android.framework.TestUser -import com.ramcosta.composedestinations.generated.app.destinations.ChannelAccessOnUpdateScreenDestination import com.wire.android.ui.home.newconversation.channelaccess.ChannelAccessType import com.wire.android.ui.home.newconversation.channelaccess.ChannelAddPermissionType -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.feature.conversation.channel.UpdateChannelAddPermissionUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery -import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.mockkObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher @@ -94,31 +89,21 @@ class UpdateChannelAccessViewModelTest { private class Arrangement { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK private lateinit var updateChannelAddPermission: UpdateChannelAddPermissionUseCase + private val conversationId = "conversationId" + private val viewModel by lazy { UpdateChannelAccessViewModel( + channelAccessNavArgs = UpdateChannelAccessArgs(conversationId), updateChannelAddPermission = updateChannelAddPermission, - savedStateHandle = savedStateHandle, qualifiedIdMapper = QualifiedIdMapper(TestUser.SELF_USER_ID) ) } init { - val conversationId = "conversationId" MockKAnnotations.init(this, relaxUnitFun = true) - mockkObject(ChannelAccessOnUpdateScreenDestination) - every { - ChannelAccessOnUpdateScreenDestination.argsFrom(any()) - } answers { - UpdateChannelAccessArgs(conversationId) - } - - every { savedStateHandle.navArgs() } returns UpdateChannelAccessArgs(conversationId) } fun withUpdateChannelAddPermissionUseCaseReturning( diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt index 421a0ff0b0d..d9729aa1d05 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelArrangement.kt @@ -18,11 +18,9 @@ package com.wire.android.ui.home.conversations.info -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.mockUri import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.ConversationNavArgs -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.common.error.StorageFailure import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId @@ -50,9 +48,6 @@ class ConversationInfoViewModelArrangement { @MockK lateinit var qualifiedIdMapper: QualifiedIdMapper - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var observeConversationDetails: ObserveConversationDetailsUseCase @@ -64,8 +59,8 @@ class ConversationInfoViewModelArrangement { private val viewModel: ConversationInfoViewModel by lazy { ConversationInfoViewModel( + conversationNavArgs = ConversationNavArgs(conversationId = conversationId), qualifiedIdMapper = qualifiedIdMapper, - savedStateHandle = savedStateHandle, observeConversationDetails = observeConversationDetails, fetchConversationMLSVerificationStatus = fetchConversationMLSVerificationStatus, selfUserId = TestUser.SELF_USER_ID, @@ -76,7 +71,6 @@ class ConversationInfoViewModelArrangement { init { MockKAnnotations.init(this, relaxUnitFun = true) mockUri() - every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId = conversationId) every { qualifiedIdMapper.fromStringToQualifiedID("some-dummy-value@some.dummy.domain") diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModelTest.kt new file mode 100644 index 00000000000..30e97f60123 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModelTest.kt @@ -0,0 +1,146 @@ +/* + * 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.conversations.media.preview + +import android.app.Application +import androidx.core.net.toUri +import com.wire.android.ui.home.conversations.model.AssetBundle +import com.wire.android.ui.sharing.ImportedMediaAsset +import com.wire.kalium.logic.data.asset.AttachmentType +import com.wire.kalium.logic.data.id.ConversationId +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import okio.Path.Companion.toPath +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +@Config(application = Application::class) +class ImagesPreviewViewModelTest { + + @Before + fun setUp() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun givenAssetUris_whenViewModelIsCreated_thenImportsAssets() = runTest { + val firstAsset = ImportedMediaAsset(testBundle("first.jpg"), assetSizeExceeded = null) + val secondAsset = ImportedMediaAsset(testBundle("second.jpg"), assetSizeExceeded = 25) + val (arrangement, viewModel) = Arrangement() + .withImportedAsset("content://asset/first", firstAsset) + .withImportedAsset("content://asset/second", secondAsset) + .arrange(assetUris = arrayListOf("content://asset/first", "content://asset/second")) + + runCurrent() + + assertEquals(2, arrangement.assetImporter.importedUris.size) + assertEquals(listOf(firstAsset, secondAsset), viewModel.viewState.assetBundleList) + assertFalse(viewModel.viewState.isLoading) + } + + @Test + fun givenAssetImportFails_whenViewModelIsCreated_thenKeepsSuccessfulAssets() = runTest { + val importedAsset = ImportedMediaAsset(testBundle("clean.jpg"), assetSizeExceeded = null) + val (arrangement, viewModel) = Arrangement() + .withImportedAsset("content://asset/clean", importedAsset) + .withImportedAsset("content://asset/broken", null) + .arrange(assetUris = arrayListOf("content://asset/clean", "content://asset/broken")) + + runCurrent() + + assertEquals(2, arrangement.assetImporter.importedUris.size) + assertEquals(listOf(importedAsset), viewModel.viewState.assetBundleList) + assertFalse(viewModel.viewState.isLoading) + } + + @Test + fun givenImportedAssets_whenSelectingAndRemovingAsset_thenUpdatesState() = runTest { + val firstAsset = ImportedMediaAsset(testBundle("first.jpg"), assetSizeExceeded = null) + val secondAsset = ImportedMediaAsset(testBundle("second.jpg"), assetSizeExceeded = null) + val (_, viewModel) = Arrangement() + .withImportedAsset("content://asset/first", firstAsset) + .withImportedAsset("content://asset/second", secondAsset) + .arrange(assetUris = arrayListOf("content://asset/first", "content://asset/second")) + runCurrent() + + viewModel.onSelected(1) + viewModel.onRemove(0) + + assertEquals(1, viewModel.viewState.selectedIndex) + assertEquals(listOf(secondAsset), viewModel.viewState.assetBundleList) + } + + private fun testBundle(fileName: String) = AssetBundle( + key = fileName, + mimeType = "image/jpeg", + dataPath = "/tmp/$fileName".toPath(), + dataSize = 100L, + fileName = fileName, + assetType = AttachmentType.IMAGE, + ) + + private class Arrangement { + val assetImporter = FakeImagesPreviewAssetImporter() + + fun withImportedAsset(@Suppress("UNUSED_PARAMETER") uri: String, importedMediaAsset: ImportedMediaAsset?) = apply { + assetImporter.assets += importedMediaAsset + } + + fun arrange( + assetUris: ArrayList = arrayListOf(), + ): Pair = this to ImagesPreviewViewModel( + navArgs = navArgs(assetUris), + assetImporter = assetImporter, + ) + + private fun navArgs(assetUris: ArrayList): ImagesPreviewNavArgs = + ImagesPreviewNavArgs( + conversationId = ConversationId("conversation-value", "conversation-domain"), + conversationName = "Conversation", + assetUriList = ArrayList(assetUris.map { it.toUri() }) + ) + } + + private class FakeImagesPreviewAssetImporter : ImagesPreviewAssetImporter { + val importedUris = mutableListOf() + val assets = mutableListOf() + + override suspend fun importAsset(uri: String): ImportedMediaAsset? { + importedUris += uri + return assets.removeAt(0) + } + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt index e6118290df9..5a4e1f48954 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt @@ -18,9 +18,7 @@ package com.wire.android.ui.home.conversations.messages -import androidx.lifecycle.SavedStateHandle import androidx.paging.PagingData -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.media.audiomessage.AudioSpeed @@ -32,7 +30,6 @@ import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.usecase.GetMessagesForConversationUseCase -import com.wire.android.util.FileManager import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.data.asset.AttachmentType import com.wire.kalium.logic.data.conversation.Conversation @@ -79,9 +76,6 @@ class ConversationMessagesViewModelArrangement { private val messagesChannel = Channel>(capacity = Channel.UNLIMITED) - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var getMessagesForConversationUseCase: GetMessagesForConversationUseCase @@ -95,7 +89,7 @@ class ConversationMessagesViewModelArrangement { lateinit var observeConversationDetails: ObserveConversationDetailsUseCase @MockK - lateinit var fileManager: FileManager + lateinit var assetFileGateway: ConversationAssetFileGateway @MockK lateinit var getMessageAsset: GetMessageAssetUseCase @@ -132,13 +126,13 @@ class ConversationMessagesViewModelArrangement { private val viewModel: ConversationMessagesViewModel by lazy { ConversationMessagesViewModel( - savedStateHandle, + ConversationNavArgs(conversationId = conversationId), observeConversationDetails, getMessageAsset, getMessageById, updateAssetMessageDownloadStatus, observeAssetStatuses, - fileManager, + assetFileGateway, TestDispatcherProvider(), getMessagesForConversationUseCase, fetchOlderNomadMessagesByConversationUseCase, @@ -157,7 +151,6 @@ class ConversationMessagesViewModelArrangement { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) mockUri() - every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId = conversationId) coEvery { toggleReaction(any(), any(), any()) } returns ToggleReactionResult.Success coEvery { observeConversationDetails(any()) } returns flowOf() coEvery { getMessagesForConversationUseCase(any(), any()) } returns messagesChannel.consumeAsFlow() @@ -190,7 +183,7 @@ class ConversationMessagesViewModelArrangement { val assetBundle = AssetBundle("key", assetMimeType, assetDataPath, assetSize, assetName, AttachmentType.fromMimeTypeString(assetMimeType)) viewModel.showOnAssetDownloadedDialog(assetBundle, messageId) - every { fileManager.openWithExternalApp(any(), any(), any()) }.answers { + every { assetFileGateway.openWithExternalApp(any(), any(), any()) }.answers { viewModel.hideOnAssetDownloadedDialog() } } @@ -258,9 +251,12 @@ class ConversationMessagesViewModelArrangement { AttachmentType.fromMimeTypeString(assetMimeType) ) viewModel.showOnAssetDownloadedDialog(assetBundle, messageId) - coEvery { fileManager.saveToExternalStorage(any(), any(), any(), any(), any()) }.answers { - viewModel.hideOnAssetDownloadedDialog() - } + coEvery { assetFileGateway.saveToExternalStorage(any(), any(), any()) } returns assetName + } + + fun withSuccessfulShareAsset(decodedAssetPath: Path, assetSize: Long, assetName: String = "name") = apply { + withGetMessageAssetUseCaseReturning(decodedAssetPath, assetSize, assetName) + every { assetFileGateway.shareWithExternalApp(any(), any()) } returns Unit } fun withFailureOnDeletingMessages() = apply { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelTest.kt index e3a603e83e5..9be43868672 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelTest.kt @@ -137,7 +137,7 @@ class ConversationMessagesViewModelTest { viewModel.downloadAndOpenAsset(messageId) // Then - verify(exactly = 1) { arrangement.fileManager.openWithExternalApp(any(), any(), any()) } + verify(exactly = 1) { arrangement.assetFileGateway.openWithExternalApp(any(), any(), any()) } assert(viewModel.conversationViewState.downloadedAssetDialogState == DownloadedAssetDialogVisibilityState.Hidden) } @@ -166,10 +166,27 @@ class ConversationMessagesViewModelTest { viewModel.downloadAssetExternally(messageId) // Then - coVerify(exactly = 1) { arrangement.fileManager.saveToExternalStorage(any(), any(), any(), any(), any()) } + coVerify(exactly = 1) { arrangement.assetFileGateway.saveToExternalStorage(any(), any(), any()) } assert(viewModel.conversationViewState.downloadedAssetDialogState == DownloadedAssetDialogVisibilityState.Hidden) } + @Test + fun `given an asset message, when sharing it, then asset file gateway shares the downloaded asset`() = runTest { + val messageId = "mocked-msg-id" + val assetName = "mocked-asset-name.zip" + val assetDataPath = "asset-data-path".toPath() + val assetSize = 8192L + val (arrangement, viewModel) = ConversationMessagesViewModelArrangement() + .withSuccessfulViewModelInit() + .withSuccessfulShareAsset(assetDataPath, assetSize, assetName) + .arrange() + + viewModel.shareAsset(messageId) + advanceUntilIdle() + + verify(exactly = 1) { arrangement.assetFileGateway.shareWithExternalApp(assetDataPath, assetName) } + } + @Test fun `given a deleted asset message, when downloading to open or save, then show already deleted dialog`() = runTest { diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt index d298e7a0c13..456b7a9871d 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt @@ -17,15 +17,11 @@ */ package com.wire.android.ui.home.conversations.messages -import android.os.Bundle import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.core.os.bundleOf -import androidx.lifecycle.SavedStateHandle import androidx.paging.PagingData import androidx.paging.map import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.config.SnapshotExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri @@ -36,13 +32,11 @@ import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesNavArgs import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesViewModel import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.id.ConversationId import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import junit.framework.TestCase.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -56,7 +50,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class, SnapshotExtension::class) +@ExtendWith(CoroutineTestExtension::class, SnapshotExtension::class) class SearchConversationMessagesViewModelTest { @Test @@ -208,17 +202,16 @@ class SearchConversationMessagesViewModelTest { private val messagesChannel = Channel>(capacity = Channel.UNLIMITED) - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var getSearchMessagesForConversation: GetConversationMessagesFromSearchUseCase private val viewModel: SearchConversationMessagesViewModel by lazy { SearchConversationMessagesViewModel( + searchConversationMessagesNavArgs = SearchConversationMessagesNavArgs( + conversationId = conversationId + ), getSearchMessagesForConversation = getSearchMessagesForConversation, - dispatchers = TestDispatcherProvider(), - savedStateHandle = savedStateHandle + dispatchers = TestDispatcherProvider() ) } @@ -226,12 +219,6 @@ class SearchConversationMessagesViewModelTest { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) mockUri() - every { savedStateHandle.navArgs() } returns SearchConversationMessagesNavArgs( - conversationId = conversationId - ) - every { savedStateHandle.get("searchConversationMessagesState") } returns bundleOf( - "value" to "" - ) coEvery { getSearchMessagesForConversation( any(), diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelTest.kt index ca31d42e995..87386ca6429 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelTest.kt @@ -17,17 +17,14 @@ */ package com.wire.android.ui.home.conversations.messages.draft -import androidx.lifecycle.SavedStateHandle import com.wire.android.R import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.config.SnapshotExtension import com.wire.android.config.mockUri import com.wire.android.framework.TestConversation import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.model.UIQuotedMessage import com.wire.android.ui.home.conversations.usecase.GetQuoteMessageForConversationUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.ui.theme.Accent import com.wire.android.util.ui.UIText import com.wire.kalium.logic.data.message.draft.MessageDraft @@ -37,7 +34,6 @@ import com.wire.kalium.logic.feature.message.draft.SaveMessageDraftUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle @@ -47,7 +43,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class, SnapshotExtension::class) +@ExtendWith(CoroutineTestExtension::class, SnapshotExtension::class) class MessageDraftViewModelTest { @Test @@ -169,14 +165,8 @@ class MessageDraftViewModelTest { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) mockUri() - every { - savedStateHandle.navArgs() - } returns ConversationNavArgs(conversationId = TestConversation.ID) } - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var getMessageDraft: GetMessageDraftUseCase @@ -188,7 +178,7 @@ class MessageDraftViewModelTest { private val viewModel by lazy { MessageDraftViewModel( - savedStateHandle, + ConversationNavArgs(conversationId = TestConversation.ID), getMessageDraft, getQuoteMessageForConversation, saveMessageDraft, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt index bfac6a4eb07..8d00a069b75 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelTest.kt @@ -17,14 +17,11 @@ */ package com.wire.android.ui.home.conversations.migration -import androidx.lifecycle.SavedStateHandle import com.wire.android.assertions.shouldBeEqualTo import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.framework.TestConversation import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.ConversationNavArgs -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.type.UserType @@ -32,7 +29,6 @@ import com.wire.kalium.logic.data.user.type.UserTypeInfo import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -40,7 +36,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) class ConversationMigrationViewModelTest { @Test @@ -79,12 +74,8 @@ class ConversationMigrationViewModelTest { @MockK lateinit var observeConversationDetailsUseCase: ObserveConversationDetailsUseCase - @MockK - lateinit var savedStateHandle: SavedStateHandle - init { MockKAnnotations.init(this) - every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId) } fun withConversationDetailsReturning(conversationDetails: ConversationDetails) = apply { @@ -94,7 +85,7 @@ class ConversationMigrationViewModelTest { fun arrange(): Pair = run { configure() - this@Arrangement to ConversationMigrationViewModel(savedStateHandle, observeConversationDetailsUseCase) + this@Arrangement to ConversationMigrationViewModel(ConversationNavArgs(conversationId), observeConversationDetailsUseCase) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelTest.kt index 5551ac93802..14203399127 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelTest.kt @@ -17,9 +17,7 @@ */ package com.wire.android.ui.home.conversations.promoteadmin -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider @@ -32,7 +30,6 @@ import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConve import com.wire.kalium.logic.feature.conversation.PromoteAdminAndLeaveConversationUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -224,9 +221,6 @@ class PromoteAdminViewModelTest { } private inner class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var observeEligibleMembers: ObserveEligibleMembersForConversationAdminRoleUseCase @@ -235,8 +229,6 @@ class PromoteAdminViewModelTest { init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.navArgs() } returns - PromoteAdminNavArgs(ConversationId("conv1", "wire.com")) coEvery { observeEligibleMembers(any()) } returns flowOf(emptyList()) coEvery { promoteAdminAndLeave(any(), any()) } returns PromoteAdminAndLeaveConversationUseCase.Result.Success } @@ -253,7 +245,7 @@ class PromoteAdminViewModelTest { observeEligibleMembers = observeEligibleMembers, promoteAdminAndLeave = promoteAdminAndLeave, dispatchers = TestDispatcherProvider(), - savedStateHandle = savedStateHandle, + navArgs = PromoteAdminNavArgs(ConversationId("conv1", "wire.com")), ) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt index 4d36908b2cc..851e430f666 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt @@ -17,15 +17,12 @@ */ package com.wire.android.ui.home.conversations.search -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.config.SnapshotExtension import com.wire.android.mapper.ContactMapper import com.wire.android.model.UserAvatarData import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.home.newconversation.model.Contact -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.EMPTY import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId @@ -57,7 +54,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class, SnapshotExtension::class) +@ExtendWith(CoroutineTestExtension::class, SnapshotExtension::class) class SearchUserViewModelTest { @Test @@ -65,7 +62,6 @@ class SearchUserViewModelTest { val query = "query" val (arrangement, viewModel) = Arrangement() - .withAddMembersSearchNavArgsThatThrowsException() .withSearchResult( SearchUserResult( connected = listOf(), @@ -239,7 +235,6 @@ class SearchUserViewModelTest { val query = "query" val (arrangement, viewModel) = Arrangement() - .withAddMembersSearchNavArgsThatThrowsException() .withSearchResult(result) .withFederatedSearchParserResult( FederatedSearchParser.Result( @@ -272,7 +267,6 @@ class SearchUserViewModelTest { fun `given search term is a valid handle, when searching, then search by handle`() = runTest { val query = "query" val (arrangement, viewModel) = Arrangement() - .withAddMembersSearchNavArgsThatThrowsException() .withSearchByHandleResult( SearchUserResult( connected = listOf(), @@ -315,7 +309,6 @@ class SearchUserViewModelTest { handle = "handle" ) val (arrangement, viewModel) = Arrangement() - .withAddMembersSearchNavArgsThatThrowsException() .withSearchByHandleResult( SearchUserResult( connected = listOf(selectedUserSearchDetails), @@ -343,9 +336,6 @@ class SearchUserViewModelTest { @MockK lateinit var contactMapper: ContactMapper - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var federatedSearchParser: FederatedSearchParser @@ -367,6 +357,8 @@ class SearchUserViewModelTest { withIsFederationSearchAllowedResult(false) } + private var addMembersSearchNavArgs: AddMembersSearchNavArgs? = null + @Suppress("CyclomaticComplexMethod") fun fromSearchUserResult(user: UserSearchDetails): Contact { with(user) { @@ -398,13 +390,7 @@ class SearchUserViewModelTest { } fun withAddMembersSearchNavArgs(navArgs: AddMembersSearchNavArgs) = apply { - every { savedStateHandle.navArgs() } returns navArgs - } - - fun withAddMembersSearchNavArgsThatThrowsException() = apply { - every { savedStateHandle.navArgs() } answers { - throw RuntimeException() - } + addMembersSearchNavArgs = navArgs } fun withSearchResult(result: SearchUserResult) = apply { @@ -431,13 +417,13 @@ class SearchUserViewModelTest { fun arrange() = apply { searchUserViewModel = SearchUserViewModel( + addMembersSearchNavArgs = addMembersSearchNavArgs, searchUserUseCase = searchUsersUseCase, searchByHandleUseCase = searchByHandleUseCase, contactMapper = contactMapper, federatedSearchParser = federatedSearchParser, validateUserHandle = validateUserHandle, - isFederationSearchAllowed = isFederationSearchAllowedUseCase, - savedStateHandle = savedStateHandle + isFederationSearchAllowed = isFederationSearchAllowedUseCase ) }.run { this to searchUserViewModel diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModelTest.kt index 4f3c62e45e2..47e66fe1e84 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModelTest.kt @@ -17,15 +17,12 @@ */ package com.wire.android.ui.home.conversations.search.adddembertoconversation -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.model.UserAvatarData import com.wire.android.ui.home.conversations.search.AddMembersSearchNavArgs import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.android.ui.home.newconversation.model.Contact -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.ConnectionState @@ -34,7 +31,6 @@ import com.wire.kalium.logic.feature.conversation.AddMemberToConversationUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.collections.immutable.persistentSetOf import kotlinx.coroutines.test.advanceUntilIdle @@ -44,7 +40,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) class AddMembersToConversationViewModelTest { @Test @@ -154,10 +149,8 @@ class AddMembersToConversationViewModelTest { @MockK lateinit var addMemberToConversationUseCase: AddMemberToConversationUseCase - @MockK - lateinit var savedStateHandle: SavedStateHandle - val testDispatchers = TestDispatcherProvider() + private lateinit var navArgs: AddMembersSearchNavArgs init { MockKAnnotations.init(this, relaxUnitFun = true) @@ -166,7 +159,7 @@ class AddMembersToConversationViewModelTest { lateinit var viewModel: AddMembersToConversationViewModel fun withAddMembersSearchNavArgs(navArgs: AddMembersSearchNavArgs) { - every { savedStateHandle.navArgs() } returns navArgs + this.navArgs = navArgs } fun withAddMemberToConversationUseCase(result: AddMemberToConversationUseCase.Result) { @@ -175,9 +168,9 @@ class AddMembersToConversationViewModelTest { fun arrange(block: Arrangement.() -> Unit): Pair = apply(block).let { viewModel = AddMembersToConversationViewModel( + addMembersSearchNavArgs = navArgs, addMemberToConversation = addMemberToConversationUseCase, - dispatchers = testDispatchers, - savedStateHandle = savedStateHandle + dispatchers = testDispatchers ) this to viewModel } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelArrangement.kt index d5326afae15..06449abf5c2 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelArrangement.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home.conversations.sendmessage -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.feature.analytics.AnonymousAnalyticsManager @@ -28,7 +27,6 @@ import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.MessageSharedState import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.ImageUtil import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.data.id.ConversationId @@ -70,9 +68,6 @@ internal class SendMessageViewModelArrangement { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) mockUri() - every { savedStateHandle.navArgs() } returns ConversationNavArgs( - conversationId = conversationId - ) // Default empty values coEvery { observeOngoingCallsUseCase() } returns flowOf(listOf()) coEvery { observeEstablishedCallsUseCase() } returns flowOf(listOf()) @@ -85,8 +80,7 @@ internal class SendMessageViewModelArrangement { coEvery { observeConversationUnderLegalHoldNotified(any()) } returns flowOf(true) } - @MockK - lateinit var savedStateHandle: SavedStateHandle + private var conversationNavArgs = ConversationNavArgs(conversationId = conversationId) @MockK lateinit var sendTextMessage: SendTextMessageUseCase @@ -161,6 +155,7 @@ internal class SendMessageViewModelArrangement { private val viewModel by lazy { SendMessageViewModel( + conversationNavArgs = conversationNavArgs, sendTextMessage = sendTextMessage, sendEditTextMessage = sendEditTextMessage, sendEditMultipartMessage = sendEditMultipartMessage, @@ -179,7 +174,6 @@ internal class SendMessageViewModelArrangement { observeConversationUnderLegalHoldNotified = observeConversationUnderLegalHoldNotified, sendLocation = sendLocation, removeMessageDraft = removeMessageDraftUseCase, - savedStateHandle = savedStateHandle, analyticsManager = analyticsManager, sendMultipartMessage = sendMultipartMessage, isWireCellsEnabledForConversation = isWireCellsEnabledForConversation, @@ -280,14 +274,14 @@ internal class SendMessageViewModelArrangement { } fun withPendingTextBundle(textToShare: String = "some text") = apply { - every { savedStateHandle.navArgs() } returns ConversationNavArgs( + conversationNavArgs = ConversationNavArgs( conversationId = conversationId, pendingTextBundle = textToShare ) } fun withPendingAssetBundle(vararg assetBundle: AssetBundle) = apply { - every { savedStateHandle.navArgs() } returns ConversationNavArgs( + conversationNavArgs = ConversationNavArgs( conversationId = conversationId, pendingBundles = arrayListOf(*assetBundle) ) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModelTest.kt index 7292949cb97..0284abbc670 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModelTest.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui.home.drawer -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.framework.TestUser @@ -94,9 +93,6 @@ class HomeDrawerViewModelTest { private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var observeArchivedUnreadConversationsCount: ObserveArchivedUnreadConversationsCountUseCase @@ -124,7 +120,6 @@ class HomeDrawerViewModelTest { } fun arrange() = this to HomeDrawerViewModel( - savedStateHandle = savedStateHandle, observeArchivedUnreadConversationsCount = { observeArchivedUnreadConversationsCount }, observeSelfUser = observeSelfUserUseCase, getTeamUrl = getTeamUrlUseCase, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt index 38f1065163f..cc8ad5ad526 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.home.gallery -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension @@ -27,7 +26,6 @@ import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.ui.home.conversations.MediaGallerySnackbarMessages import com.wire.android.ui.home.conversations.delete.DeleteMessageDialogState import com.wire.android.ui.home.conversations.delete.DeleteMessageDialogType -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.FileManager import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetMessageAttachmentUseCase @@ -54,7 +52,6 @@ import com.wire.kalium.logic.feature.message.MessageOperationResult import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -379,9 +376,6 @@ class MediaGalleryViewModelTest { } private class Arrangement { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var getConversationDetails: ObserveConversationDetailsUseCase @@ -400,24 +394,24 @@ class MediaGalleryViewModelTest { @MockK lateinit var getCellFile: GetCellFileUseCase + private var navArgs = MediaGalleryNavArgs( + conversationId = dummyConversationId, + messageId = dummyMessageId, + isSelfAsset = true, + isEphemeral = false, + messageOptionsEnabled = true, + cellAssetId = null, + ) + init { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.navArgs() } returns MediaGalleryNavArgs( - conversationId = dummyConversationId, - messageId = dummyMessageId, - isSelfAsset = true, - isEphemeral = false, - messageOptionsEnabled = true, - cellAssetId = null, - ) - coEvery { deleteMessage(any(), any(), any()) } returns MessageOperationResult.Success } fun withNavArgs(messageOptionsEnabled: Boolean = true, isEphemeral: Boolean = false, cellAssetId: String? = null) = apply { - every { savedStateHandle.navArgs() } returns MediaGalleryNavArgs( + navArgs = MediaGalleryNavArgs( conversationId = dummyConversationId, messageId = dummyMessageId, isSelfAsset = true, @@ -478,7 +472,7 @@ class MediaGalleryViewModelTest { } fun arrange() = this to MediaGalleryViewModel( - savedStateHandle, + navArgs, getConversationDetails, TestDispatcherProvider(), getImageData, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt index 9d9350da14a..c88ece9a923 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt @@ -17,7 +17,7 @@ */ package com.wire.android.ui.home.messagecomposer.recordaudio -import android.content.Context +import android.net.Uri import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider @@ -47,9 +47,11 @@ import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import okio.Path import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import java.io.File @ExtendWith(CoroutineTestExtension::class) class RecordAudioViewModelTest { @@ -112,13 +114,7 @@ class RecordAudioViewModelTest { viewModel.stopRecording() // then - coVerify(exactly = 0) { - arrangement.generateAudioFileWithEffects( - context = any(), - originalFilePath = any(), - effectsFilePath = any() - ) - } + assertEquals(0, arrangement.recordAudioFileGateway.generateAudioFileWithEffectsCalls) assertEquals( RecordAudioButtonState.READY_TO_SEND, viewModel.state.buttonState @@ -139,13 +135,7 @@ class RecordAudioViewModelTest { viewModel.stopRecording() // then - coVerify(exactly = 1) { - arrangement.generateAudioFileWithEffects( - context = any(), - originalFilePath = any(), - effectsFilePath = any(), - ) - } + assertEquals(1, arrangement.recordAudioFileGateway.generateAudioFileWithEffectsCalls) assertEquals( RecordAudioButtonState.READY_TO_SEND, viewModel.state.buttonState @@ -163,25 +153,13 @@ class RecordAudioViewModelTest { viewModel.startRecording() viewModel.stopRecording() assertEquals(null, viewModel.state.effectsOutputFile) - coVerify(exactly = 0) { - arrangement.generateAudioFileWithEffects( - context = any(), - originalFilePath = any(), - effectsFilePath = any() - ) - } + assertEquals(0, arrangement.recordAudioFileGateway.generateAudioFileWithEffectsCalls) // when viewModel.setShouldApplyEffects(true) // then - coVerify(exactly = 1) { - arrangement.generateAudioFileWithEffects( - context = any(), - originalFilePath = any(), - effectsFilePath = any() - ) - } + assertEquals(1, arrangement.recordAudioFileGateway.generateAudioFileWithEffectsCalls) assert(viewModel.state.effectsOutputFile != null) assertEquals( RecordAudioButtonState.READY_TO_SEND, @@ -371,8 +349,7 @@ class RecordAudioViewModelTest { val currentScreenManager = mockk() val getAssetSizeLimit = mockk() val globalDataStore = mockk() - val generateAudioFileWithEffects = mockk() - val context = mockk() + val recordAudioFileGateway = FakeRecordAudioFileGateway() val dispatchers = TestDispatcherProvider() val fakeKaliumFileSystem = FakeKaliumFileSystem() val audioNormalizedLoudnessBuilder = mockk() @@ -380,13 +357,12 @@ class RecordAudioViewModelTest { val viewModel by lazy { RecordAudioViewModel( - context = context, recordAudioMessagePlayer = recordAudioMessagePlayer, observeEstablishedCalls = observeEstablishedCalls, currentScreenManager = currentScreenManager, audioMediaRecorder = audioMediaRecorder, getAssetSizeLimit = getAssetSizeLimit, - generateAudioFileWithEffects = generateAudioFileWithEffects, + recordAudioFileGateway = recordAudioFileGateway, globalDataStore = globalDataStore, dispatchers = dispatchers, audioNormalizedLoudnessBuilder = audioNormalizedLoudnessBuilder, @@ -411,7 +387,6 @@ class RecordAudioViewModelTest { maxSize = ASSET_SIZE_DEFAULT_LIMIT_BYTES ) ) - coEvery { generateAudioFileWithEffects(any(), any(), any()) } returns Unit coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow( CurrentScreen.Conversation(id = DUMMY_CALL.conversationId) @@ -448,6 +423,22 @@ class RecordAudioViewModelTest { fun arrange() = this to viewModel + class FakeRecordAudioFileGateway : RecordAudioFileGateway { + var generateAudioFileWithEffectsCalls = 0 + private set + + override suspend fun generateAudioFileWithEffects( + originalFilePath: String, + effectsFilePath: String + ) { + generateAudioFileWithEffectsCalls++ + } + + override fun audioLengthInMs(audioPath: Path): Long = 1L + + override fun contentUri(audioFile: File): Uri = mockk() + } + companion object { const val ASSET_SIZE_LIMIT = 5L val DUMMY_CALL = Call( diff --git a/app/src/test/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModelTest.kt new file mode 100644 index 00000000000..e842d1151f7 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModelTest.kt @@ -0,0 +1,59 @@ +/* + * 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.settings.about.dependencies + +import com.wire.android.config.CoroutineTestExtension +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +class DependenciesViewModelTest { + + @Test + fun givenDependenciesInfoProvider_whenViewModelIsCreated_thenStateUsesProvidedDependencies() = runTest { + val viewModel = DependenciesViewModel( + dependenciesInfoProvider = FakeDependenciesInfoProvider( + dependencies = mapOf( + "coil" to "3.0.0", + "local-tooling" to null + ) + ) + ) + advanceUntilIdle() + + assertEquals( + DependenciesState( + dependencies = persistentMapOf( + "coil" to "3.0.0", + "local-tooling" to null + ) + ), + viewModel.state + ) + } + + private class FakeDependenciesInfoProvider( + private val dependencies: Map + ) : DependenciesInfoProvider { + override suspend fun dependenciesVersion(): Map = dependencies + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModelTest.kt new file mode 100644 index 00000000000..a7f5e2508e7 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModelTest.kt @@ -0,0 +1,63 @@ +/* + * 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.settings.about.licenses + +import com.mikepenz.aboutlibraries.entity.Library +import com.wire.android.config.CoroutineTestExtension +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +class LicensesViewModelTest { + + @Test + fun givenLicensesProvider_whenViewModelIsCreated_thenStateUsesProvidedLibraries() = runTest { + val libraries = listOf( + library(uniqueId = "kotlinx-coroutines", name = "Kotlinx Coroutines"), + library(uniqueId = "aboutlibraries", name = "AboutLibraries") + ) + + val viewModel = LicensesViewModel( + licensesProvider = FakeLicensesProvider(libraries) + ) + + assertEquals( + LicensesState(libraryList = libraries), + viewModel.state + ) + } + + private class FakeLicensesProvider( + private val libraries: List + ) : LicensesProvider { + override suspend fun getLibraries(): List = libraries + } + + private fun library(uniqueId: String, name: String): Library = Library( + uniqueId = uniqueId, + artifactVersion = null, + name = name, + description = null, + website = null, + developers = emptyList(), + organization = null, + scm = null + ) +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/settings/account/email/VerifyEmailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/settings/account/email/VerifyEmailViewModelTest.kt index 05b7c686138..39164f8a00a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/settings/account/email/VerifyEmailViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/settings/account/email/VerifyEmailViewModelTest.kt @@ -17,17 +17,13 @@ */ package com.wire.android.ui.home.settings.account.email -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.ui.home.settings.account.email.verifyEmail.VerifyEmailNavArgs import com.wire.android.ui.home.settings.account.email.verifyEmail.VerifyEmailViewModel -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.feature.user.UpdateEmailUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -36,9 +32,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(NavigationTestExtension::class) class VerifyEmailViewModelTest { @Test @@ -58,18 +52,17 @@ class VerifyEmailViewModelTest { private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var updateEmail: UpdateEmailUseCase + private var verifyEmailNavArgs = VerifyEmailNavArgs(newEmail = "newEmail") + init { MockKAnnotations.init(this, relaxUnitFun = true) } fun withNewEmail(email: String) = apply { - every { savedStateHandle.navArgs() } returns VerifyEmailNavArgs(newEmail = email) + verifyEmailNavArgs = VerifyEmailNavArgs(newEmail = email) } fun withUpdateEmailResult(result: UpdateEmailUseCase.Result) = apply { @@ -78,7 +71,7 @@ class VerifyEmailViewModelTest { fun arrange() = this to VerifyEmailViewModel( updateEmail, - savedStateHandle + verifyEmailNavArgs ) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModelTest.kt index 41f4935fae3..b73ff3a8a91 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModelTest.kt @@ -17,10 +17,8 @@ */ package com.wire.android.ui.home.settings.appsettings.networkSettings -import android.content.Context import com.wire.android.config.CoroutineTestExtension import com.wire.android.emm.ManagedConfigurationsManager -import com.wire.android.util.isWebsocketEnabledByDefault import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.auth.PersistentWebSocketStatus import com.wire.kalium.logic.data.id.QualifiedID @@ -30,13 +28,12 @@ import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSo import com.wire.kalium.logic.feature.user.webSocketStatus.PersistPersistentWebSocketConnectionStatusUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -46,33 +43,27 @@ import org.junit.jupiter.api.extension.ExtendWith class NetworkSettingsViewModelTest { private lateinit var viewModel: NetworkSettingsViewModel - private lateinit var context: Context private lateinit var persistPersistentWebSocketConnectionStatus: PersistPersistentWebSocketConnectionStatusUseCase private lateinit var observePersistentWebSocketConnectionStatus: ObservePersistentWebSocketConnectionStatusUseCase private lateinit var currentSession: CurrentSessionUseCase private lateinit var managedConfigurationsManager: ManagedConfigurationsManager + private lateinit var networkSettingsDefaultsProvider: FakeNetworkSettingsDefaultsProvider @BeforeEach fun setup() { MockKAnnotations.init(this, relaxUnitFun = true) - mockkStatic(::isWebsocketEnabledByDefault) - context = mockk(relaxed = true) persistPersistentWebSocketConnectionStatus = mockk() observePersistentWebSocketConnectionStatus = mockk() currentSession = mockk() managedConfigurationsManager = mockk() - } - - @AfterEach - fun tearDown() { - io.mockk.unmockkStatic(::isWebsocketEnabledByDefault) + networkSettingsDefaultsProvider = FakeNetworkSettingsDefaultsProvider() } @Test fun `given websocket is enabled by default, when ViewModel initializes, then state reflects it`() = runTest { // given - every { isWebsocketEnabledByDefault(context) } returns true + networkSettingsDefaultsProvider.isWebSocketEnabledByDefault = true coEvery { currentSession() } returns CurrentSessionResult.Success( AccountInfo.Valid(userId = QualifiedID("user", "domain")) ) @@ -92,7 +83,7 @@ class NetworkSettingsViewModelTest { @Test fun `given websocket is not enabled by default, when ViewModel initializes, then state reflects it`() = runTest { // given - every { isWebsocketEnabledByDefault(context) } returns false + networkSettingsDefaultsProvider.isWebSocketEnabledByDefault = false coEvery { currentSession() } returns CurrentSessionResult.Success( AccountInfo.Valid(userId = QualifiedID("user", "domain")) ) @@ -113,7 +104,7 @@ class NetworkSettingsViewModelTest { fun `given websocket is enabled, when ViewModel observes status, then state reflects it`() = runTest { // given val userId = QualifiedID("user", "domain") - every { isWebsocketEnabledByDefault(context) } returns false + networkSettingsDefaultsProvider.isWebSocketEnabledByDefault = false coEvery { currentSession() } returns CurrentSessionResult.Success(AccountInfo.Valid(userId = userId)) coEvery { observePersistentWebSocketConnectionStatus() } returns ObservePersistentWebSocketConnectionStatusUseCase.Result.Success( @@ -132,7 +123,7 @@ class NetworkSettingsViewModelTest { fun `given websocket is disabled, when ViewModel observes status, then state reflects it`() = runTest { // given val userId = QualifiedID("user", "domain") - every { isWebsocketEnabledByDefault(context) } returns false + networkSettingsDefaultsProvider.isWebSocketEnabledByDefault = false coEvery { currentSession() } returns CurrentSessionResult.Success(AccountInfo.Valid(userId = userId)) coEvery { observePersistentWebSocketConnectionStatus() } returns ObservePersistentWebSocketConnectionStatusUseCase.Result.Success( @@ -150,7 +141,7 @@ class NetworkSettingsViewModelTest { @Test fun `given MDM enforces websocket, when ViewModel observes MDM state, then state reflects it`() = runTest { // given - every { isWebsocketEnabledByDefault(context) } returns false + networkSettingsDefaultsProvider.isWebSocketEnabledByDefault = false coEvery { currentSession() } returns CurrentSessionResult.Success( AccountInfo.Valid(userId = QualifiedID("user", "domain")) ) @@ -171,7 +162,7 @@ class NetworkSettingsViewModelTest { @Test fun `given MDM does not enforce websocket, when ViewModel observes MDM state, then state reflects it`() = runTest { // given - every { isWebsocketEnabledByDefault(context) } returns false + networkSettingsDefaultsProvider.isWebSocketEnabledByDefault = false coEvery { currentSession() } returns CurrentSessionResult.Success( AccountInfo.Valid(userId = QualifiedID("user", "domain")) ) @@ -191,7 +182,7 @@ class NetworkSettingsViewModelTest { @Test fun `given websocket is not enforced by MDM, when setting websocket state to enabled, then persist is called`() = runTest { // given - every { isWebsocketEnabledByDefault(context) } returns false + networkSettingsDefaultsProvider.isWebSocketEnabledByDefault = false coEvery { currentSession() } returns CurrentSessionResult.Success( AccountInfo.Valid(userId = QualifiedID("user", "domain")) ) @@ -208,13 +199,13 @@ class NetworkSettingsViewModelTest { viewModel.setWebSocketState(true) // then - coEvery { persistPersistentWebSocketConnectionStatus(true) } + coVerify { persistPersistentWebSocketConnectionStatus(true) } } @Test fun `given websocket is enforced by MDM, when attempting to set websocket state, then persist is not called`() = runTest { // given - every { isWebsocketEnabledByDefault(context) } returns false + networkSettingsDefaultsProvider.isWebSocketEnabledByDefault = false coEvery { currentSession() } returns CurrentSessionResult.Success( AccountInfo.Valid(userId = QualifiedID("user", "domain")) ) @@ -230,13 +221,13 @@ class NetworkSettingsViewModelTest { viewModel.setWebSocketState(false) // then - persist should not be called - coEvery { persistPersistentWebSocketConnectionStatus(any()) } returns Unit + coVerify(exactly = 0) { persistPersistentWebSocketConnectionStatus(any()) } } @Test fun `given no current session, when ViewModel initializes, then state has default values`() = runTest { // given - every { isWebsocketEnabledByDefault(context) } returns false + networkSettingsDefaultsProvider.isWebSocketEnabledByDefault = false coEvery { currentSession() } returns CurrentSessionResult.Failure.SessionNotFound coEvery { observePersistentWebSocketConnectionStatus() } returns ObservePersistentWebSocketConnectionStatusUseCase.Result.Success( @@ -256,6 +247,10 @@ class NetworkSettingsViewModelTest { observePersistentWebSocketConnectionStatus = observePersistentWebSocketConnectionStatus, currentSession = currentSession, managedConfigurationsManager = managedConfigurationsManager, - context = context + networkSettingsDefaultsProvider = networkSettingsDefaultsProvider ) + + private class FakeNetworkSettingsDefaultsProvider : NetworkSettingsDefaultsProvider { + override var isWebSocketEnabledByDefault: Boolean = false + } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/settings/home/BackupAndRestoreViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/settings/home/BackupAndRestoreViewModelTest.kt index 125e07c7cdb..eabe1458df3 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/settings/home/BackupAndRestoreViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/settings/home/BackupAndRestoreViewModelTest.kt @@ -18,21 +18,19 @@ package com.wire.android.ui.home.settings.home -import android.net.Uri import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.core.net.toUri import com.wire.android.config.SnapshotExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.UserDataStore import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.ui.home.settings.backup.BackupAndRestoreState import com.wire.android.ui.home.settings.backup.BackupAndRestoreViewModel +import com.wire.android.ui.home.settings.backup.BackupFileGateway import com.wire.android.ui.home.settings.backup.BackupCreationProgress import com.wire.android.ui.home.settings.backup.BackupRestoreProgress import com.wire.android.ui.home.settings.backup.MPBackupSettings import com.wire.android.ui.home.settings.backup.PasswordValidation import com.wire.android.ui.home.settings.backup.RestoreFileValidation -import com.wire.android.util.FileManager import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.feature.auth.ValidatePasswordResult import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase @@ -55,8 +53,6 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.mockkStatic import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -67,6 +63,7 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import kotlinx.datetime.Instant import okio.IOException +import okio.Path import okio.Path.Companion.toPath import okio.buffer import org.junit.jupiter.api.AfterEach @@ -224,13 +221,7 @@ class BackupAndRestoreViewModelTest { ), backupAndRestoreViewModel.state ) - coVerify(exactly = 1) { - arrangement.fileManager.shareWithExternalApp( - storedBackup.path, - storedBackup.assetName, - any() - ) - } + assertEquals(listOf(storedBackup.path to storedBackup.assetName), arrangement.backupFileGateway.sharedBackups) coVerify { arrangement.userDataStore.setLastBackupDateSeconds(any()) } @@ -244,7 +235,7 @@ class BackupAndRestoreViewModelTest { .withPreviouslyCreatedBackup(storedBackup) .withUpdateLastBackupData() .arrange() - val backupUri = "some-backup".toUri() + val backupUri = "some-backup" // When backupAndRestoreViewModel.saveBackup(backupUri) @@ -256,13 +247,7 @@ class BackupAndRestoreViewModelTest { BackupAndRestoreState.INITIAL_STATE.copy(lastBackupData = backupAndRestoreViewModel.state.lastBackupData), backupAndRestoreViewModel.state ) - coVerify(exactly = 1) { - arrangement.fileManager.copyToUri( - storedBackup.path, - backupUri, - any() - ) - } + assertEquals(listOf(storedBackup.path to backupUri), arrangement.backupFileGateway.savedBackups) coVerify(exactly = 1) { arrangement.userDataStore.setLastBackupDateSeconds(any()) } @@ -275,7 +260,7 @@ class BackupAndRestoreViewModelTest { val (arrangement, backupAndRestoreViewModel) = Arrangement() .withSuccessfulDBImport(isBackupEncrypted) .arrange() - val backupUri = "some-backup".toUri() + val backupUri = "some-backup" // When backupAndRestoreViewModel.chooseBackupFileToRestore(backupUri) @@ -285,9 +270,7 @@ class BackupAndRestoreViewModelTest { assert(backupAndRestoreViewModel.state.backupRestoreProgress == BackupRestoreProgress.Finished) assert(backupAndRestoreViewModel.state.restoreFileValidation == RestoreFileValidation.ValidNonEncryptedBackup) assert(arrangement.fakeKaliumFileSystem.exists(backupAndRestoreViewModel.latestImportedBackupTempPath)) - coVerify(exactly = 1) { - arrangement.fileManager.copyToPath(backupUri, backupAndRestoreViewModel.latestImportedBackupTempPath, any()) - } + assertEquals(listOf(backupUri), arrangement.backupFileGateway.importedBackupUris) } @Test @@ -297,7 +280,7 @@ class BackupAndRestoreViewModelTest { val (arrangement, backupAndRestoreViewModel) = Arrangement() .withSuccessfulDBImport(isBackupEncrypted) .arrange() - val backupUri = "some-backup".toUri() + val backupUri = "some-backup" // When backupAndRestoreViewModel.chooseBackupFileToRestore(backupUri) @@ -306,9 +289,7 @@ class BackupAndRestoreViewModelTest { // Then assert(backupAndRestoreViewModel.state.restoreFileValidation == RestoreFileValidation.PasswordRequired) assert(arrangement.fakeKaliumFileSystem.exists(backupAndRestoreViewModel.latestImportedBackupTempPath)) - coVerify(exactly = 1) { - arrangement.fileManager.copyToPath(backupUri, backupAndRestoreViewModel.latestImportedBackupTempPath, any()) - } + assertEquals(listOf(backupUri), arrangement.backupFileGateway.importedBackupUris) } @Test @@ -317,7 +298,7 @@ class BackupAndRestoreViewModelTest { val (arrangement, backupAndRestoreViewModel) = Arrangement() .withFailedBackupVerification() .arrange() - val backupUri = "some-backup".toUri() + val backupUri = "some-backup" // When backupAndRestoreViewModel.chooseBackupFileToRestore(backupUri) @@ -326,15 +307,13 @@ class BackupAndRestoreViewModelTest { // Then assert(backupAndRestoreViewModel.state.restoreFileValidation == RestoreFileValidation.IncompatibleBackup) assert(arrangement.fakeKaliumFileSystem.exists(backupAndRestoreViewModel.latestImportedBackupTempPath)) - coVerify(exactly = 1) { - arrangement.fileManager.copyToPath(backupUri, backupAndRestoreViewModel.latestImportedBackupTempPath, any()) - } + assertEquals(listOf(backupUri), arrangement.backupFileGateway.importedBackupUris) } @Test fun givenAStoredBackup_whenThereIsAnErrorImportingTheDB_thenTheRightErrorDialogIsShown() = runTest(dispatcher.default()) { // Given - val backupUri = "some-backup".toUri() + val backupUri = "some-backup" val (arrangement, backupAndRestoreViewModel) = Arrangement() .withFailedDBImport() .arrange() @@ -347,19 +326,16 @@ class BackupAndRestoreViewModelTest { assert(backupAndRestoreViewModel.state.restoreFileValidation == RestoreFileValidation.IncompatibleBackup) assert(backupAndRestoreViewModel.state.backupRestoreProgress == BackupRestoreProgress.Failed) assert(arrangement.fakeKaliumFileSystem.exists(backupAndRestoreViewModel.latestImportedBackupTempPath)) - coVerify(exactly = 1) { - arrangement.fileManager.copyToPath(backupUri, backupAndRestoreViewModel.latestImportedBackupTempPath, any()) - } + assertEquals(listOf(backupUri), arrangement.backupFileGateway.importedBackupUris) } @Test fun givenARestoreDialogShown_whenDismissingIt_thenTheTempImportedBackupPathIsDeleted() = runTest(dispatcher.default()) { // Given - val mockUri = "some-backup" val (arrangement, backupAndRestoreViewModel) = Arrangement() .withSuccessfulDBImport(false) .arrange() - val backupUri = mockUri.toUri() + val backupUri = "some-backup" // When backupAndRestoreViewModel.chooseBackupFileToRestore(backupUri) @@ -372,9 +348,11 @@ class BackupAndRestoreViewModelTest { assert(backupAndRestoreViewModel.state.backupRestoreProgress == BackupRestoreProgress.InProgress(0f)) assert(backupAndRestoreViewModel.state.restorePasswordValidation == PasswordValidation.NotVerified) assert(!arrangement.fakeKaliumFileSystem.exists(backupAndRestoreViewModel.latestImportedBackupTempPath)) - coVerify(exactly = 1) { - arrangement.fileManager.copyToPath(backupUri, backupAndRestoreViewModel.latestImportedBackupTempPath, any()) - } + assertEquals(listOf(backupUri), arrangement.backupFileGateway.importedBackupUris) + assertEquals( + listOf(backupAndRestoreViewModel.latestImportedBackupTempPath), + arrangement.backupFileGateway.deletedImportedBackups + ) } @Test @@ -499,10 +477,7 @@ class BackupAndRestoreViewModelTest { init { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) - val mockUri = mockk() - mockkStatic(Uri::class) withGetLastBackupDateSeconds() - every { Uri.parse("some-backup") } returns mockUri coEvery { importBackup(any(), any()) } returns RestoreBackupResult.Success coEvery { createMpBackupFile(any(), any()) } returns CreateBackupResult.Success("".toPath(), "") coEvery { verifyBackup(any()) } returns VerifyBackupResult.Success(BackupFileFormat.ANDROID, true) @@ -526,22 +501,19 @@ class BackupAndRestoreViewModelTest { @MockK lateinit var validatePassword: ValidatePasswordUseCase - @MockK - lateinit var fileManager: FileManager - @MockK lateinit var userDataStore: UserDataStore val fakeKaliumFileSystem = FakeKaliumFileSystem() + val backupFileGateway = FakeBackupFileGateway(fakeKaliumFileSystem) private val viewModel = BackupAndRestoreViewModel( importBackup = importBackup, importMpBackup = importMpBackup, createMpBackupFile = createMpBackupFile, verifyBackup = verifyBackup, - kaliumFileSystem = fakeKaliumFileSystem, dispatcher = dispatcher, - fileManager = fileManager, + backupFileGateway = backupFileGateway, validatePassword = validatePassword, userDataStore = userDataStore, createBackupFile = createBackupFile, @@ -572,18 +544,12 @@ class BackupAndRestoreViewModelTest { } fun withRequestedPasswordDialog() = apply { + viewModel.latestImportedBackupTempPath = + fakeKaliumFileSystem.tempFilePath(BackupAndRestoreViewModel.TEMP_IMPORTED_BACKUP_FILE_NAME) viewModel.state = viewModel.state.copy(restoreFileValidation = RestoreFileValidation.PasswordRequired) } fun withSuccessfulDBImport(isEncrypted: Boolean) = apply { - coEvery { fileManager.copyToPath(any(), any(), any()) } returns (100L).also { - viewModel.latestImportedBackupTempPath = - fakeKaliumFileSystem.tempFilePath(BackupAndRestoreViewModel.TEMP_IMPORTED_BACKUP_FILE_NAME) - fakeKaliumFileSystem.sink(viewModel.latestImportedBackupTempPath).buffer().use { - it.write("someBackupData".toByteArray()) - } - } - coEvery { verifyBackup(any()) } returns VerifyBackupResult.Success( format = BackupFileFormat.ANDROID, @@ -593,26 +559,10 @@ class BackupAndRestoreViewModelTest { } fun withFailedBackupVerification() = apply { - coEvery { fileManager.copyToPath(any(), any(), any()) } returns (100L).also { - viewModel.latestImportedBackupTempPath = - fakeKaliumFileSystem.tempFilePath(BackupAndRestoreViewModel.TEMP_IMPORTED_BACKUP_FILE_NAME) - fakeKaliumFileSystem.sink(viewModel.latestImportedBackupTempPath).buffer().use { - it.write("someBackupData".toByteArray()) - } - } - coEvery { verifyBackup(any()) } returns VerifyBackupResult.Failure.InvalidBackupFile } fun withFailedDBImport(error: Failure = Failure(IncompatibleBackup("DB failed to import"))) = apply { - coEvery { fileManager.copyToPath(any(), any(), any()) } returns (100L).also { - viewModel.latestImportedBackupTempPath = - fakeKaliumFileSystem.tempFilePath(BackupAndRestoreViewModel.TEMP_IMPORTED_BACKUP_FILE_NAME) - fakeKaliumFileSystem.sink(viewModel.latestImportedBackupTempPath).buffer().use { - it.write("someBackupData".toByteArray()) - } - } - coEvery { verifyBackup(any()) } returns VerifyBackupResult.Success( format = BackupFileFormat.ANDROID, isEncrypted = false @@ -638,4 +588,40 @@ class BackupAndRestoreViewModelTest { fun arrange() = this to viewModel } + + private class FakeBackupFileGateway( + private val fakeKaliumFileSystem: FakeKaliumFileSystem + ) : BackupFileGateway { + + val sharedBackups = mutableListOf>() + val savedBackups = mutableListOf>() + val importedBackupUris = mutableListOf() + val deletedImportedBackups = mutableListOf() + + override suspend fun shareBackup(path: Path, assetName: String?) { + sharedBackups += path to assetName + } + + override suspend fun saveBackup(path: Path, destinationUri: String) { + savedBackups += path to destinationUri + } + + override suspend fun importBackupToTempPath(sourceUri: String): Path { + importedBackupUris += sourceUri + return fakeKaliumFileSystem + .tempFilePath(BackupAndRestoreViewModel.TEMP_IMPORTED_BACKUP_FILE_NAME) + .also { path -> + fakeKaliumFileSystem.sink(path).buffer().use { + it.write("someBackupData".toByteArray()) + } + } + } + + override suspend fun deleteImportedBackup(path: Path) { + deletedImportedBackups += path + if (fakeKaliumFileSystem.exists(path)) { + fakeKaliumFileSystem.delete(path) + } + } + } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelTest.kt index d8a0404cbea..03e12afdb48 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelTest.kt @@ -17,16 +17,15 @@ */ package com.wire.android.ui.home.whatsnew -import android.content.Context import com.prof18.rssparser.RssParser import com.prof18.rssparser.model.RssChannel import com.prof18.rssparser.model.RssItem -import com.wire.android.R import com.wire.android.config.CoroutineTestExtension import com.wire.android.util.toMediumOnlyDateTime import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockkStatic import kotlinx.coroutines.test.advanceUntilIdle @@ -76,17 +75,17 @@ class WhatsNewViewModelTest { inner class Arrangement { @MockK - lateinit var context: Context + lateinit var releaseNotesFeedUrlProvider: ReleaseNotesFeedUrlProvider @MockK lateinit var rssParser: RssParser val viewModel by lazy { - WhatsNewViewModel(context) + WhatsNewViewModel(releaseNotesFeedUrlProvider) } fun withFeedUrl(feedUrl: String) = apply { - coEvery { context.resources.getString(R.string.url_android_release_notes_feed) } returns feedUrl + every { releaseNotesFeedUrlProvider.feedUrl } returns feedUrl } fun withFeedResult(rssChannel: RssChannel) = apply { diff --git a/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/LoginFlowStateHolderTest.kt b/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/LoginFlowStateHolderTest.kt new file mode 100644 index 00000000000..3f0dbd5b9d1 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/LoginFlowStateHolderTest.kt @@ -0,0 +1,89 @@ +/* + * 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.newauthentication.login + +import app.cash.turbine.test +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class LoginFlowStateHolderTest { + + @Test + fun givenEmptyInput_whenCreated_thenNextIsDisabled() { + val holder = LoginFlowStateHolder() + + assertEquals(LoginFlowHolderState(), holder.state.value) + assertFalse(holder.state.value.nextEnabled) + } + + @Test + fun givenNonEmptyInput_whenCreated_thenNextIsEnabled() { + val holder = LoginFlowStateHolder(initialUserIdentifier = "user@example.com") + + assertEquals("user@example.com", holder.userIdentifier) + assertTrue(holder.state.value.nextEnabled) + } + + @Test + fun givenLoadingState_whenInputIsUpdated_thenNextStaysDisabled() { + val holder = LoginFlowStateHolder(initialFlowState = NewLoginFlowState.Loading) + + holder.updateUserIdentifier("user@example.com") + + assertEquals("user@example.com", holder.userIdentifier) + assertEquals(NewLoginFlowState.Loading, holder.flowState) + assertFalse(holder.state.value.nextEnabled) + } + + @Test + fun givenTextFieldError_whenInputIsUpdated_thenTextFieldErrorIsCleared() { + val holder = LoginFlowStateHolder( + initialFlowState = NewLoginFlowState.Error.TextFieldError.InvalidValue + ) + + holder.updateUserIdentifier("user@example.com") + + assertEquals(NewLoginFlowState.Default, holder.flowState) + } + + @Test + fun givenDialogError_whenInputIsUpdated_thenDialogErrorIsKept() { + val holder = LoginFlowStateHolder( + initialFlowState = NewLoginFlowState.Error.DialogError.InvalidSSOCode + ) + + holder.updateUserIdentifier("user@example.com") + + assertEquals(NewLoginFlowState.Error.DialogError.InvalidSSOCode, holder.flowState) + } + + @Test + fun givenResultCollector_whenResultIsEmitted_thenCollectorReceivesIt() = runTest { + val holder = LoginFlowStateHolder() + + holder.results.test { + holder.emitResult("next") + + assertEquals("next", awaitItem()) + } + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/LoginNavigatorTest.kt b/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/LoginNavigatorTest.kt new file mode 100644 index 00000000000..099db0c0ba2 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/LoginNavigatorTest.kt @@ -0,0 +1,87 @@ +/* + * 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.newauthentication.login + +import com.wire.android.ui.authentication.login.LoginPasswordPath +import com.wire.android.ui.authentication.login.sso.SSOUrlConfig +import com.wire.android.util.newServerConfig +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class LoginNavigatorTest { + + @Test + fun `given enterprise unsupported action, when mapped, then return enterprise unsupported command`() { + val action = NewLoginAction.EnterpriseLoginNotSupported(USER_IDENTIFIER) + + val result = action.toLoginNavigationCommand() + + assertEquals(LoginNavigationCommand.EnterpriseLoginNotSupported(USER_IDENTIFIER), result) + } + + @Test + fun `given email password action, when mapped, then return email password command`() { + val loginPasswordPath = LoginPasswordPath(isCloudAccountCreationPossible = true) + val action = NewLoginAction.EmailPassword(USER_IDENTIFIER, loginPasswordPath) + + val result = action.toLoginNavigationCommand() + + assertEquals(LoginNavigationCommand.EmailPassword(USER_IDENTIFIER, loginPasswordPath), result) + } + + @Test + fun `given custom config action, when mapped, then return custom config command`() { + val customServerConfig = newServerConfig(1).links + val action = NewLoginAction.CustomConfig(USER_IDENTIFIER, customServerConfig) + + val result = action.toLoginNavigationCommand() + + assertEquals(LoginNavigationCommand.CustomConfig(USER_IDENTIFIER, customServerConfig), result) + } + + @Test + fun `given sso action, when mapped, then return sso command`() { + val config = SSOUrlConfig(USER_IDENTIFIER) + val action = NewLoginAction.SSO(SSO_URL, config) + + val result = action.toLoginNavigationCommand() + + assertEquals(LoginNavigationCommand.SSO(SSO_URL, config), result) + } + + @Test + fun `given success action, when mapped, then return success command`() { + val expectedResults = mapOf( + NewLoginAction.Success.NextStep.E2EIEnrollment to LoginNavigationCommand.Success.NextStep.E2EIEnrollment, + NewLoginAction.Success.NextStep.InitialSync to LoginNavigationCommand.Success.NextStep.InitialSync, + NewLoginAction.Success.NextStep.TooManyDevices to LoginNavigationCommand.Success.NextStep.TooManyDevices, + NewLoginAction.Success.NextStep.None to LoginNavigationCommand.Success.NextStep.None, + ) + + expectedResults.forEach { (actionNextStep, commandNextStep) -> + val result = NewLoginAction.Success(actionNextStep).toLoginNavigationCommand() + + assertEquals(LoginNavigationCommand.Success(commandNextStep), result) + } + } + + private companion object { + const val USER_IDENTIFIER = "user@wire.com" + const val SSO_URL = "https://wire.com/sso" + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelTest.kt index e783b6a2da2..b46b4e2af80 100644 --- a/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelTest.kt @@ -1,11 +1,9 @@ package com.wire.android.ui.newauthentication.login +import android.database.sqlite.SQLiteException import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.config.SnapshotExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.UserDataStoreProvider @@ -14,6 +12,7 @@ import com.wire.android.framework.TestClient import com.wire.android.ui.authentication.login.DomainClaimedByOrg import com.wire.android.ui.authentication.login.LoginNavArgs import com.wire.android.ui.authentication.login.LoginPasswordPath +import com.wire.android.ui.authentication.login.LoginSavedInputStore import com.wire.android.ui.authentication.login.LoginViewModelExtension import com.wire.android.ui.authentication.login.PreFilledUserIdentifierType import com.wire.android.ui.authentication.login.SSOCodeAutoLogin @@ -43,12 +42,10 @@ import com.wire.kalium.logic.feature.backup.RestoreCryptoStateResult import com.wire.kalium.logic.feature.backup.RestoreCryptoStateUseCase import com.wire.kalium.logic.feature.backup.SetLastDeviceIdResult import com.wire.kalium.logic.feature.backup.SetLastDeviceIdUseCase -import android.database.sqlite.SQLiteException import com.wire.kalium.logic.feature.client.RegisterClientResult import com.wire.kalium.logic.feature.session.DeleteSessionUseCase import com.wire.kalium.logic.feature.session.DoesValidSessionExistResult import com.wire.kalium.logic.feature.session.DoesValidSessionExistUseCase -import java.io.IOException import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -62,10 +59,11 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertInstanceOf import org.junit.jupiter.api.extension.ExtendWith +import java.io.IOException @Suppress("LargeClass") @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(CoroutineTestExtension::class, SnapshotExtension::class, NavigationTestExtension::class) +@ExtendWith(CoroutineTestExtension::class, SnapshotExtension::class) class NewLoginViewModelTest { private val dispatchers = TestDispatcherProvider() @@ -507,6 +505,36 @@ class NewLoginViewModelTest { expected = NewLoginAction.EmailPassword(email, LoginPasswordPath(isCloudAccountCreationPossible = true)), ) + @Test + fun `given custom server nav args, when enterprise login uses password path, then keep custom server config`() = + runTest(dispatchers.main()) { + val serverConfig = newServerConfig(2).links + val (arrangement, viewModel) = Arrangement() + .withNavArgsServerConfig(serverConfig) + .withAuthenticationScopeSuccess() + .withGetLoginFlowForDomainReturning(EnterpriseLoginResult.Success(LoginRedirectPath.Default)) + .arrange() + + viewModel.actions.test { + viewModel.getEnterpriseLoginFlow(email) + advanceUntilIdle() + + assertEquals( + NewLoginAction.EmailPassword( + userIdentifier = email, + loginPasswordPath = LoginPasswordPath( + customServerConfig = serverConfig, + isCloudAccountCreationPossible = true, + ) + ), + expectMostRecentItem() + ) + } + coVerify(exactly = 1) { + arrangement.authenticationScope.getLoginFlowForDomainUseCase(email) + } + } + @Test fun `given no registration path, when enterprise login, then call EmailPassword action with no creation`() = testEnterpriseLoginActions( @@ -627,7 +655,7 @@ class NewLoginViewModelTest { lateinit var loginSSOViewModelExtension: LoginSSOViewModelExtension @MockK - private lateinit var savedStateHandle: SavedStateHandle + private lateinit var savedInputStore: LoginSavedInputStore @MockK private lateinit var clientScopeProviderFactory: ClientScopeProvider.Factory @@ -657,23 +685,14 @@ class NewLoginViewModelTest { init { MockKAnnotations.init(this, relaxUnitFun = true) - every { - savedStateHandle.get(any()) - } returns null - every { - savedStateHandle[any()] = any() - } returns Unit - every { - savedStateHandle.navArgs() - } returns LoginNavArgs() + every { savedInputStore.userIdentifier } returns null + every { savedInputStore.userIdentifier = any() } returns Unit every { coreLogic.getGlobalScope().deleteSession } returns deleteSessionUseCase every { coreLogic.getSessionScope(any()).logout } returns logoutUseCase } fun withNavArgsServerConfig(serverConfig: ServerConfig.Links) = apply { - every { - savedStateHandle.navArgs() - } returns LoginNavArgs(loginPasswordPath = LoginPasswordPath(serverConfig)) + loginNavArgs = LoginNavArgs(loginPasswordPath = LoginPasswordPath(serverConfig)) } fun withEmailOrSSOCodeValidatorReturning(result: ValidateEmailOrSSOCodeUseCase.Result = ValidEmail) = apply { @@ -797,9 +816,7 @@ class NewLoginViewModelTest { } fun withNomadAutoLogin(nomadServiceUrl: String) = apply { - every { - savedStateHandle.navArgs() - } returns LoginNavArgs( + loginNavArgs = LoginNavArgs( ssoCodeAutoLogin = SSOCodeAutoLogin( ssoCode = "wire-sso-code", nomadServiceUrl = nomadServiceUrl, @@ -809,24 +826,16 @@ class NewLoginViewModelTest { } fun withEmptyUserIdentifierAndNoPreFilledIdentifier() = apply { - every { - savedStateHandle.get(any()) - } returns null - every { - savedStateHandle.navArgs() - } returns LoginNavArgs() + every { savedInputStore.userIdentifier } returns null + loginNavArgs = LoginNavArgs() } fun withUserIdentifierAlreadySet(userIdentifier: String) = apply { - every { - savedStateHandle.get(any()) - } returns userIdentifier + every { savedInputStore.userIdentifier } returns userIdentifier } fun withPreFilledUserIdentifier(userIdentifier: String) = apply { - every { - savedStateHandle.navArgs() - } returns LoginNavArgs(userHandle = PreFilledUserIdentifierType.PreFilled(userIdentifier)) + loginNavArgs = LoginNavArgs(userHandle = PreFilledUserIdentifierType.PreFilled(userIdentifier)) } fun withFetchDefaultSSOCodeSuccessAfterDelay(defaultSSOCode: String?) = apply { @@ -844,9 +853,7 @@ class NewLoginViewModelTest { } fun withCustomServerConfigDeepLink() = apply { - every { - savedStateHandle.navArgs() - } returns LoginNavArgs( + loginNavArgs = LoginNavArgs( loginPasswordPath = LoginPasswordPath( customServerConfig = ServerConfig.STAGING ) @@ -879,18 +886,21 @@ class NewLoginViewModelTest { } private var defaultSSOCodeConfig: String = String.EMPTY + private var loginNavArgs: LoginNavArgs = LoginNavArgs() fun arrange() = this to NewLoginViewModel( + loginNavArgs, validateEmailOrSSOCodeUseCase, coreLogic, - savedStateHandle, + savedInputStore, clientScopeProviderFactory, userDataStoreProvider, loginViewModelExtension, loginSSOViewModelExtension, dispatchers, ServerConfig.STAGING, - defaultSSOCodeConfig + defaultSSOCodeConfig, + NewLoginRecoverableLogoutExceptionDetector() ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelTest.kt index 0aabcdd0d36..ab74bbe0f8a 100644 --- a/app/src/test/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelTest.kt @@ -1,7 +1,6 @@ package com.wire.android.ui.registration.details import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.lifecycle.SavedStateHandle import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension @@ -10,7 +9,6 @@ import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.ui.authentication.create.common.CreateAccountDataNavArgs import com.wire.android.ui.authentication.create.common.UserRegistrationInfo -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.auth.AuthenticationScope @@ -23,7 +21,6 @@ import com.wire.kalium.logic.feature.register.RequestActivationCodeUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle @@ -161,8 +158,7 @@ class CreateAccountDataDetailViewModelTest { } private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle + private val createAccountDataNavArgs = CreateAccountDataNavArgs(userRegistrationInfo = UserRegistrationInfo()) @MockK lateinit var validateEmailUseCase: ValidateEmailUseCase @@ -190,8 +186,6 @@ class CreateAccountDataDetailViewModelTest { init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.navArgs() } returns - CreateAccountDataNavArgs(userRegistrationInfo = UserRegistrationInfo()) coEvery { coreLogic.versionedAuthenticationScope(any()) } returns autoVersionAuthScopeUseCase coEvery { @@ -218,7 +212,7 @@ class CreateAccountDataDetailViewModelTest { } fun arrange() = this to CreateAccountDataDetailViewModel( - savedStateHandle = savedStateHandle, + createAccountNavArgs = createAccountDataNavArgs, validateEmail = validateEmailUseCase, validatePassword = validatePasswordUseCase, coreLogic = coreLogic, diff --git a/app/src/test/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelTest.kt index f438127e62f..4dcf49c10bb 100644 --- a/app/src/test/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelTest.kt @@ -1,13 +1,9 @@ package com.wire.android.ui.registration.selector -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.datastore.GlobalDataStore -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.kalium.logic.configuration.server.ServerConfig import io.mockk.MockKAnnotations -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -16,7 +12,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class) +@ExtendWith(CoroutineTestExtension::class) class CreateAccountSelectorViewModelTest { @Test @@ -46,20 +42,16 @@ class CreateAccountSelectorViewModelTest { @MockK lateinit var globalDataStore: GlobalDataStore - @MockK - lateinit var savedStateHandle: SavedStateHandle + private var navArgs = CreateAccountSelectorNavArgs(ServerConfig.STAGING) init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.navArgs() } returns - CreateAccountSelectorNavArgs(ServerConfig.STAGING) } fun withEmailNavArgs(email: String) = apply { - every { savedStateHandle.navArgs() } returns - CreateAccountSelectorNavArgs(ServerConfig.STAGING, email) + navArgs = CreateAccountSelectorNavArgs(ServerConfig.STAGING, email) } - fun arrange() = this to CreateAccountSelectorViewModel(globalDataStore, savedStateHandle, ServerConfig.STAGING) + fun arrange() = this to CreateAccountSelectorViewModel(navArgs, globalDataStore, ServerConfig.STAGING) } } diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModelTest.kt new file mode 100644 index 00000000000..5192f9635c7 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModelTest.kt @@ -0,0 +1,53 @@ +/* + * 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.settings.about + +import com.wire.android.config.CoroutineTestExtension +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +class AboutThisAppViewModelTest { + + @Test + fun givenAppInfoProvider_whenViewModelIsCreated_thenStateUsesProvidedAppInfo() = runTest { + val viewModel = AboutThisAppViewModel( + aboutThisAppInfoProvider = FakeAboutThisAppInfoProvider( + appName = "5.0.0-123-dev", + gitBuildId = "abc123" + ) + ) + + assertEquals( + AboutThisAppState( + appName = "5.0.0-123-dev", + commitish = "abc123" + ), + viewModel.state + ) + } + + private class FakeAboutThisAppInfoProvider( + override val appName: String, + private val gitBuildId: String + ) : AboutThisAppInfoProvider { + override fun gitBuildId(): String = gitBuildId + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt index 53c6be6c51c..be006e1bbd6 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt @@ -19,15 +19,13 @@ package com.wire.android.ui.settings.debug -import android.content.Context import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.ScopedArgsTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.framework.TestUser +import com.wire.android.ui.debug.DebugDataInfoProvider import com.wire.android.ui.debug.DebugDataOptionsViewModelImpl -import com.wire.android.util.getDeviceIdString -import com.wire.android.util.getGitBuildId import com.wire.android.util.ui.UIText import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.logic.configuration.server.CommonApiVersionType @@ -56,10 +54,8 @@ import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk -import io.mockk.mockkStatic import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -79,7 +75,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given token sending token will succeed, when sending FCM token, then info message should emmit success message`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withSendFCMTokenSuccess() .arrange() @@ -96,7 +92,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given there is not client ID, when sending FCM token,info message should emit error message`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withSendFCMTokenClientIdFailure() .arrange() @@ -113,7 +109,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given there is not notification token, when sending FCM token,info message should emit error message`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withSendFCMTokenNotificationTokenFailure() .arrange() @@ -130,7 +126,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given that there is API failure, when sending FCM token,info message should emit error message`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withSendFCMTokenClientRepositoryRegisterTokenFailure() .arrange() @@ -147,7 +143,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given that Proteus protocol is used, view state should have Proteus protocol name`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withProteusProtocolSetup() .arrange() @@ -157,7 +153,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given that Mls protocol is used, view state should have proteus Mls name`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withMlsProtocolSetup() .arrange() @@ -167,7 +163,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given that federation is disabled, view state should have federation value of false`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withFederationDisabled() .arrange() @@ -177,7 +173,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given that federation is enabled, view state should have federation value of true`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withFederationEnabled() .arrange() @@ -187,7 +183,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given that api version is unknown, view state should have api version unknown`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withApiVersionUnknown() .arrange() @@ -197,17 +193,30 @@ class DebugDataOptionsViewModelTest { @Test fun `given that api version is set, view state should have api version set`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withApiVersionSet(7) .arrange() assertEquals("7", viewModel.state.currentApiVersion) } + @Test + fun `given debug data info is available, view state should contain debug id and commitish`() = runTest { + val (_, viewModel) = DebugDataOptionsArrangement() + .withDebugDataInfo( + deviceId = "fakeDeviceId", + gitBuildId = "fakeGitBuildId" + ) + .arrange() + + assertEquals("fakeDeviceId", viewModel.state.debugId) + assertEquals("fakeGitBuildId", viewModel.state.commitish) + } + @Test fun `given server config failure, view state should have default values`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withServerConfigError() .arrange() @@ -218,7 +227,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given async notifications is not enabled, when enabling, then start using async notifications is called`() = runTest { // given - val (arrangement, viewModel) = DebugDataOptionsHiltArrangement() + val (arrangement, viewModel) = DebugDataOptionsArrangement() .withObserveIsConsumableNotificationsEnabled(false) .withStartUsingAsyncNotificationsResult() .arrange() @@ -234,7 +243,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given async notifications is enabled, then start using async notifications is never called`() = runTest { // given - val (arrangement, viewModel) = DebugDataOptionsHiltArrangement() + val (arrangement, viewModel) = DebugDataOptionsArrangement() .withObserveIsConsumableNotificationsEnabled(true) .withStartUsingAsyncNotificationsResult() .arrange() @@ -249,7 +258,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given e2ei expiration is loaded, view state should contain loaded value`() = runTest { - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withDebugE2EICertificateExpiration(999) .arrange() @@ -258,7 +267,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given default e2ei expiration is loaded, then minimum debug value is applied`() = runTest { - val (arrangement, viewModel) = DebugDataOptionsHiltArrangement() + val (arrangement, viewModel) = DebugDataOptionsArrangement() .withDebugE2EICertificateExpiration(90.days.inWholeSeconds) .arrange() @@ -268,7 +277,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given expiration below minimum, when updating e2ei expiration, then minimum value is used`() = runTest { - val (arrangement, viewModel) = DebugDataOptionsHiltArrangement().arrange() + val (arrangement, viewModel) = DebugDataOptionsArrangement().arrange() viewModel.updateE2EICertificateExpiration(120) @@ -278,7 +287,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given valid expiration value, when updating e2ei expiration, then value is updated and use case is called`() = runTest { - val (arrangement, viewModel) = DebugDataOptionsHiltArrangement() + val (arrangement, viewModel) = DebugDataOptionsArrangement() .withDebugE2EICertificateExpiration(360) .arrange() @@ -289,10 +298,9 @@ class DebugDataOptionsViewModelTest { } } -internal class DebugDataOptionsHiltArrangement { +internal class DebugDataOptionsArrangement { - @MockK(relaxed = true) - lateinit var context: Context + private var debugDataInfoProvider = FakeDebugDataInfoProvider() private val currentAccount: UserId = TestUser.SELF_USER_ID @@ -337,7 +345,7 @@ internal class DebugDataOptionsHiltArrangement { private val viewModel by lazy { DebugDataOptionsViewModelImpl( - context = context, + debugDataInfoProvider = debugDataInfoProvider, currentAccount = currentAccount, updateApiVersions = updateApiVersions, mlsKeyPackageCount = mlsKeyPackageCount, @@ -359,7 +367,6 @@ internal class DebugDataOptionsHiltArrangement { init { MockKAnnotations.init(this, relaxUnitFun = true) Dispatchers.setMain(UnconfinedTestDispatcher()) - mockkStatic("com.wire.android.util.FileUtilKt") runBlocking { coEvery { mlsKeyPackageCount() @@ -367,12 +374,6 @@ internal class DebugDataOptionsHiltArrangement { coEvery { getCurrentAnalyticsTrackingIdentifier() } returns "trackingId" - every { - context.getDeviceIdString() - } returns "deviceId" - every { - context.getGitBuildId() - } returns "gitBuildId" coEvery { selfServerConfigUseCase() } returns SelfServerConfigUseCase.Result.Success( @@ -400,6 +401,16 @@ internal class DebugDataOptionsHiltArrangement { coEvery { getDebugE2EICertificateExpiration() } returns expiration } + fun withDebugDataInfo( + deviceId: String? = "deviceId", + gitBuildId: String = "gitBuildId" + ) = apply { + debugDataInfoProvider = FakeDebugDataInfoProvider( + deviceId = deviceId, + gitBuildId = gitBuildId + ) + } + suspend fun withObserveIsConsumableNotificationsEnabled(isEnabled: Boolean = false) = apply { coEvery { observeIsConsumableNotificationsEnabled() @@ -522,3 +533,11 @@ internal class DebugDataOptionsHiltArrangement { fun arrange() = this to viewModel } + +private class FakeDebugDataInfoProvider( + private val deviceId: String? = "deviceId", + private val gitBuildId: String = "gitBuildId" +) : DebugDataInfoProvider { + override fun deviceId(): String? = deviceId + override fun gitBuildId(): String = gitBuildId +} diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt index 2ca63f65bb0..ffc80d69f1c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt @@ -17,15 +17,12 @@ */ package com.wire.android.ui.settings.devices -import androidx.lifecycle.SavedStateHandle import com.wire.android.assertIs import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension import com.wire.android.framework.TestClient import com.wire.android.framework.TestUser import com.wire.android.ui.authentication.devices.remove.RemoveDeviceDialogState import com.wire.android.ui.authentication.devices.remove.RemoveDeviceError -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.ui.settings.devices.DeviceDetailsViewModelTest.Arrangement.Companion.CLIENT_ID import com.wire.android.ui.settings.devices.DeviceDetailsViewModelTest.Arrangement.Companion.MLS_CLIENT_IDENTITY_WITH_VALID_E2EI import com.wire.kalium.common.error.CoreFailure @@ -57,7 +54,6 @@ import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -71,7 +67,6 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) class DeviceDetailsViewModelTest { @Test @@ -329,9 +324,6 @@ class DeviceDetailsViewModelTest { private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var deleteClientUseCase: DeleteClientUseCase @@ -361,9 +353,14 @@ class DeviceDetailsViewModelTest { val currentUserId = UserId("currentUserId", "currentUserDomain") + private var deviceDetailsNavArgs = DeviceDetailsNavArgs( + userId = currentUserId, + clientId = CLIENT_ID + ) + val viewModel by lazy { DeviceDetailsViewModel( - savedStateHandle = savedStateHandle, + deviceDetailsNavArgs = deviceDetailsNavArgs, deleteClient = deleteClientUseCase, observeClientDetails = observeClientDetails, isPasswordRequired = isPasswordRequiredUseCase, @@ -412,7 +409,7 @@ class DeviceDetailsViewModelTest { } fun withRequiredMockSetup(userId: UserId = currentUserId) = apply { - every { savedStateHandle.navArgs() } returns DeviceDetailsNavArgs( + deviceDetailsNavArgs = DeviceDetailsNavArgs( userId = userId, clientId = CLIENT_ID ) diff --git a/app/src/test/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelTest.kt index d244a9b55e5..5e0c0482ab5 100644 --- a/app/src/test/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelTest.kt @@ -23,11 +23,11 @@ import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.SnapshotExtension import com.wire.android.config.TestDispatcherProvider -import com.wire.android.config.mockUri import com.wire.android.framework.TestConversationItem import com.wire.android.framework.TestUser +import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase -import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase +import com.wire.kalium.logic.data.asset.AttachmentType import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase @@ -39,6 +39,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import okio.Path.Companion.toPath +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -70,7 +73,92 @@ class ImportMediaAuthenticatedViewModelTest { } } - inner class Arrangement { + @Test + fun `given shared text, when handling received content, then import text without importing assets`() = + runTest(dispatcherProvider.main()) { + // Given + val (arrangement, viewModel) = Arrangement().arrange() + + // When + viewModel.handleReceivedDataFromSharingIntent( + ImportMediaSharingContent(text = "shared text", assetUris = emptyList()) + ) + + // Then + assertEquals("shared text", viewModel.importMediaState.importedText) + assertTrue(viewModel.importMediaState.importedAssets.isEmpty()) + assertEquals(emptyList(), arrangement.importMediaAssetImporter.importedUris) + } + + @Test + fun `given single shared asset, when handling received content, then import asset`() = runTest(dispatcherProvider.main()) { + // Given + val importedAsset = ImportedMediaAsset(testBundle("image.jpg"), assetSizeExceeded = null) + val (arrangement, viewModel) = Arrangement() + .withImportedAsset("content://asset/1", importedAsset) + .arrange() + + // When + viewModel.handleReceivedDataFromSharingIntent( + ImportMediaSharingContent(text = null, assetUris = listOf("content://asset/1")) + ) + + // Then + assertEquals(listOf("content://asset/1"), arrangement.importMediaAssetImporter.importedUris) + assertEquals(listOf(importedAsset), viewModel.importMediaState.importedAssets) + } + + @Test + fun `given shared asset exceeds max size, when handling received content, then emit snackbar`() = runTest(dispatcherProvider.main()) { + // Given + val importedAsset = ImportedMediaAsset(testBundle("large.mov"), assetSizeExceeded = 25) + val (_, viewModel) = Arrangement() + .withImportedAsset("content://asset/large", importedAsset) + .arrange() + + // Then + viewModel.infoMessage.test { + // When + viewModel.handleReceivedDataFromSharingIntent( + ImportMediaSharingContent(text = null, assetUris = listOf("content://asset/large")) + ) + + assertTrue(awaitItem() is SendMessagesSnackbarMessages.MaxAssetSizeExceeded) + } + } + + @Test + fun `given multiple shared assets, when one fails to import, then keep successful assets`() = runTest(dispatcherProvider.main()) { + // Given + val importedAsset = ImportedMediaAsset(testBundle("clean.pdf"), assetSizeExceeded = null) + val (arrangement, viewModel) = Arrangement() + .withImportedAsset("content://asset/clean", importedAsset) + .withImportedAsset("content://asset/broken", null) + .arrange() + + // When + viewModel.handleReceivedDataFromSharingIntent( + ImportMediaSharingContent( + text = null, + assetUris = listOf("content://asset/clean", "content://asset/broken") + ) + ) + + // Then + assertEquals(listOf("content://asset/clean", "content://asset/broken"), arrangement.importMediaAssetImporter.importedUris) + assertEquals(listOf(importedAsset), viewModel.importMediaState.importedAssets) + } + + private fun testBundle(fileName: String) = AssetBundle( + key = "key", + mimeType = "text/plain", + dataPath = "/tmp/file".toPath(), + dataSize = 100L, + fileName = fileName, + assetType = AttachmentType.GENERIC_FILE, + ) + + private inner class Arrangement { @MockK lateinit var getSelfUser: ObserveSelfUserUseCase @@ -78,8 +166,7 @@ class ImportMediaAuthenticatedViewModelTest { @MockK lateinit var getConversationsPaginated: GetConversationsFromSearchUseCase - @MockK - lateinit var handleUriAssetUseCase: HandleUriAssetUseCase + val importMediaAssetImporter = FakeImportMediaAssetImporter() @MockK lateinit var persistNewSelfDeletionTimerUseCase: PersistNewSelfDeletionTimerUseCase @@ -97,16 +184,29 @@ class ImportMediaAuthenticatedViewModelTest { coEvery { getSelfUser.invoke() } returns flowOf(TestUser.SELF_USER) - mockUri() + } + + fun withImportedAsset(uri: String, importedMediaAsset: ImportedMediaAsset?) = apply { + importMediaAssetImporter.assets[uri] = importedMediaAsset } fun arrange() = this to ImportMediaAuthenticatedViewModel( getSelf = getSelfUser, getConversationsPaginated = getConversationsPaginated, - handleUriAsset = handleUriAssetUseCase, + importMediaAssetImporter = importMediaAssetImporter, persistNewSelfDeletionTimerUseCase = persistNewSelfDeletionTimerUseCase, observeSelfDeletionSettingsForConversation = observeSelfDeletionSettingsForConversation, dispatchers = dispatcherProvider, ) } + + private class FakeImportMediaAssetImporter : ImportMediaAssetImporter { + val importedUris = mutableListOf() + val assets = mutableMapOf() + + override suspend fun importAsset(uri: String): ImportedMediaAsset? { + importedUris += uri + return assets[uri] + } + } } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/image/AvatarPickerViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/image/AvatarPickerViewModelTest.kt index 7dec830259c..9b2302b69a2 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/image/AvatarPickerViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/image/AvatarPickerViewModelTest.kt @@ -18,18 +18,13 @@ package com.wire.android.ui.userprofile.image -import android.content.Context -import android.net.Uri import app.cash.turbine.test import com.wire.android.assertIs import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.UserDataStore import com.wire.android.framework.FakeKaliumFileSystem +import com.wire.android.ui.userprofile.avatarpicker.AvatarImageGateway import com.wire.android.ui.userprofile.avatarpicker.AvatarPickerViewModel -import com.wire.android.util.AvatarImageManager -import com.wire.android.util.resampleImageAndCopyToTempPath -import com.wire.android.util.toByteArray import com.wire.kalium.common.error.CoreFailure.Unknown import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper @@ -45,10 +40,10 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk -import io.mockk.mockkStatic import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runTest +import okio.Path import okio.buffer import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf @@ -77,7 +72,7 @@ class AvatarPickerViewModelTest { // Then with(arrangement) { coVerify { - uploadUserAvatarUseCase(any(), any()) + uploadUserAvatarUseCase(any(), 5L) userDataStore.updateUserAvatarAssetId(uploadedAssetId.toString()) } assertIs(avatarPickerViewModel.pictureState) @@ -102,11 +97,9 @@ class AvatarPickerViewModelTest { // Then with(arrangement) { coVerify { - uploadUserAvatarUseCase(any(), any()) - } - coVerify(exactly = 1) { - avatarImageManager.getWritableAvatarUri(any()) + uploadUserAvatarUseCase(any(), 5L) } + assertEquals(1, avatarImageGateway.writableAvatarUriCalls) assertIs(avatarPickerViewModel.pictureState) // not PictureState.Completed } @@ -147,6 +140,7 @@ class AvatarPickerViewModelTest { .arrange() avatarPickerViewModel.updatePickedAvatarUri(arrangement.mockOriginalUri, arrangement.mockTargetUri) + assertEquals(listOf(arrangement.mockOriginalUri), arrangement.avatarImageGateway.sanitizedAvatarUris) assertInstanceOf(AvatarPickerViewModel.PictureState.Picked::class.java, avatarPickerViewModel.pictureState) avatarPickerViewModel.loadInitialAvatarState() assertInstanceOf(AvatarPickerViewModel.PictureState.Initial::class.java, avatarPickerViewModel.pictureState) @@ -159,6 +153,7 @@ class AvatarPickerViewModelTest { .arrange() avatarPickerViewModel.updatePickedAvatarUri(arrangement.mockOriginalUri, arrangement.mockTargetUri) + assertEquals(listOf(arrangement.mockOriginalUri), arrangement.avatarImageGateway.sanitizedAvatarUris) assertInstanceOf(AvatarPickerViewModel.PictureState.Picked::class.java, avatarPickerViewModel.pictureState) avatarPickerViewModel.loadInitialAvatarState() assertInstanceOf(AvatarPickerViewModel.PictureState.Empty::class.java, avatarPickerViewModel.pictureState) @@ -172,30 +167,24 @@ class AvatarPickerViewModelTest { val uploadUserAvatarUseCase = mockk() - val avatarImageManager = mockk() - - val context = mockk() + val avatarImageGateway = FakeAvatarImageGateway() @MockK private lateinit var qualifiedIdMapper: QualifiedIdMapper - val dispatcherProvider = TestDispatcherProvider() - val viewModel by lazy { AvatarPickerViewModel( userDataStore, getAvatarAsset, uploadUserAvatarUseCase, - avatarImageManager, - dispatcherProvider, + avatarImageGateway, fakeKaliumFileSystem, - qualifiedIdMapper, - context + qualifiedIdMapper ) } - val mockTargetUri = mockk() - val mockOriginalUri = mockk() + val mockTargetUri = "file://target-avatar" + val mockOriginalUri = "content://original-avatar" init { MockKAnnotations.init(this, relaxUnitFun = true) @@ -203,20 +192,13 @@ class AvatarPickerViewModelTest { fun withSuccessfulInitialAvatarLoad(): Arrangement { val avatarAssetId = "avatar-value@avatar-domain" - mockkStatic(Uri::class) - mockkStatic(Uri::resampleImageAndCopyToTempPath) - mockkStatic(Uri::toByteArray) - every { Uri.parse(any()) } returns mockTargetUri val fakeAvatarData = "some-dummy-avatar".toByteArray() val avatarPath = fakeKaliumFileSystem.selfUserAvatarPath() fakeKaliumFileSystem.sink(avatarPath).buffer().use { it.write(fakeAvatarData) } coEvery { getAvatarAsset(any()) } returns PublicAssetResult.Success(avatarPath) - coEvery { avatarImageManager.getWritableAvatarUri(any()) } returns mockTargetUri - coEvery { avatarImageManager.getShareableTempAvatarUri(any()) } returns mockTargetUri - coEvery { any().resampleImageAndCopyToTempPath(any(), any(), any(), eq(true), any()) } returns 1L - coEvery { any().toByteArray(any(), any()) } returns ByteArray(5) + avatarImageGateway.imageSize = 5L every { userDataStore.avatarAssetId } returns flow { emit(avatarAssetId) } every { qualifiedIdMapper.fromStringToQualifiedID(any()) } returns QualifiedID("avatar-value", "avatar-domain") @@ -226,7 +208,6 @@ class AvatarPickerViewModelTest { fun withFailedInitialAvatarLoad(): Arrangement { val avatarAssetId = "avatar-value@avatar-domain" coEvery { getAvatarAsset(any()) } returns PublicAssetResult.Failure(Unknown(RuntimeException("some error")), false) - coEvery { avatarImageManager.getShareableTempAvatarUri(any()) } returns mockTargetUri every { userDataStore.avatarAssetId } returns flow { emit(avatarAssetId) } every { qualifiedIdMapper.fromStringToQualifiedID(any()) } returns QualifiedID("avatar-value", "avatar-domain") @@ -234,7 +215,6 @@ class AvatarPickerViewModelTest { } fun withNoInitialAvatar(): Arrangement { - coEvery { avatarImageManager.getShareableTempAvatarUri(any()) } returns mockTargetUri every { userDataStore.avatarAssetId } returns flow { emit(null) } return this @@ -259,7 +239,28 @@ class AvatarPickerViewModelTest { this to viewModel } + private class FakeAvatarImageGateway : AvatarImageGateway { + var imageSize: Long = 0L + var writableAvatarUriCalls: Int = 0 + private set + val sanitizedAvatarUris = mutableListOf() + + override fun getWritableAvatarUri(avatarPath: Path): String { + writableAvatarUriCalls++ + return TARGET_AVATAR_URI + } + + override fun getShareableTempAvatarUri(avatarPath: Path): String = TARGET_AVATAR_URI + + override suspend fun sanitizeAvatarImage(originalAvatarUri: String, avatarPath: Path) { + sanitizedAvatarUris += originalAvatarUri + } + + override suspend fun getAvatarImageSize(avatarUri: String): Long = imageSize + } + companion object { val fakeKaliumFileSystem: FakeKaliumFileSystem = FakeKaliumFileSystem() + const val TARGET_AVATAR_URI: String = "file://target-avatar" } } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt index 88fa67d13d5..644cc540caa 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt @@ -59,7 +59,7 @@ class OtherUserProfileScreenViewModelTest { // given val expected = OtherUserProfileGroupState("some_name", Member.Role.Member, false, CONVERSATION_ID) val (arrangement, viewModel) = OtherUserProfileViewModelArrangement() - .withConversationIdInSavedState(CONVERSATION_ID) + .withConversationIdInArgs(CONVERSATION_ID) .withGetOneToOneConversation(GetOneToOneConversationDetailsUseCase.Result.Success(CONVERSATION_ONE_ONE)) .arrange() @@ -78,7 +78,7 @@ class OtherUserProfileScreenViewModelTest { fun `given no conversationId, when loading the data, then return null group state`() = runTest { // given val (arrangement, viewModel) = OtherUserProfileViewModelArrangement() - .withConversationIdInSavedState(null) + .withConversationIdInArgs(null) .arrange() // when diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt index 900703f9ed3..7c709fe7de3 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt @@ -18,14 +18,12 @@ package com.wire.android.ui.userprofile.other -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.framework.TestUser import com.wire.android.mapper.UserTypeMapper import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveConversationRoleForUserUseCase import com.wire.android.ui.home.conversationslist.model.Membership -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModelTest.Companion.CONVERSATION_ID import com.wire.android.ui.userprofile.other.OtherUserProfileScreenViewModelTest.Companion.USER_ID import com.wire.kalium.logic.data.id.ConversationId @@ -52,9 +50,6 @@ import kotlinx.coroutines.flow.flowOf internal class OtherUserProfileViewModelArrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var getOneToOneConversation: GetOneToOneConversationDetailsUseCase @@ -97,8 +92,14 @@ internal class OtherUserProfileViewModelArrangement { @MockK lateinit var isE2EIEnabled: IsE2EIEnabledUseCase + private var navArgs = OtherUserProfileNavArgs( + groupConversationId = CONVERSATION_ID, + userId = USER_ID + ) + private val viewModel by lazy { OtherUserProfileScreenViewModel( + navArgs, TestDispatcherProvider(), observeUserInfo, userTypeMapper, @@ -111,7 +112,6 @@ internal class OtherUserProfileViewModelArrangement { isOneToOneConversationCreated, mlsClientIdentity, isE2EIEnabled, - savedStateHandle, ) } @@ -119,11 +119,6 @@ internal class OtherUserProfileViewModelArrangement { MockKAnnotations.init(this, relaxUnitFun = true) mockUri() - every { savedStateHandle.navArgs() } returns OtherUserProfileNavArgs( - groupConversationId = CONVERSATION_ID, - userId = USER_ID - ) - coEvery { observeConversationRoleForUserUseCase.invoke(any(), any()) } returns flowOf(OtherUserProfileScreenViewModelTest.CONVERSATION_ROLE_DATA) @@ -148,8 +143,8 @@ internal class OtherUserProfileViewModelArrangement { coEvery { updateConversationMemberRoleUseCase(any(), any(), any()) } returns result } - fun withConversationIdInSavedState(conversationId: ConversationId?) = apply { - every { savedStateHandle.navArgs() } returns OtherUserProfileNavArgs( + fun withConversationIdInArgs(conversationId: ConversationId?) = apply { + navArgs = OtherUserProfileNavArgs( userId = USER_ID, groupConversationId = conversationId ) diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt index 48d16e47e03..2bd16305d5f 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt @@ -1,22 +1,15 @@ package com.wire.android.ui.userprofile.qr -import android.content.Context -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension -import com.wire.android.config.NavigationTestExtension -import com.wire.android.config.TestDispatcherProvider import com.wire.android.feature.analytics.AnonymousAnalyticsManager -import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.framework.TestUser -import com.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.newServerConfig import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery -import io.mockk.every +import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -25,7 +18,6 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) class SelfQRCodeViewModelTest { @Test fun `given user is on self qr code screen, then data is loaded correctly`() = runTest { @@ -44,36 +36,49 @@ class SelfQRCodeViewModelTest { ) } - private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle + @Test + fun `given qr image when sharing asset then asset repository saves it and returns share uri`() = runTest { + // given + val (arrangement, viewModel) = Arrangement() + .withQRCodeAssetUri("content://wire/qr") + .arrange() + val qrImage = SelfQRCodeImage { } + + // when + val result = viewModel.shareQRAsset(qrImage) + + // then + assertEquals("content://wire/qr", result) + coVerify(exactly = 1) { arrangement.qrAssetRepository.saveQRCode(qrImage) } + } + private class Arrangement { @MockK lateinit var selfServerConfig: SelfServerConfigUseCase @MockK lateinit var analyticsManager: AnonymousAnalyticsManager - val context = mockk() + @MockK + lateinit var qrAssetRepository: SelfQRCodeAssetRepository init { MockKAnnotations.init(this, relaxUnitFun = true) coEvery { selfServerConfig.invoke() } returns SelfServerConfigUseCase.Result.Success( serverLinks = newServerConfig(1).copy(links = ServerConfig.STAGING) ) - every { savedStateHandle.navArgs() } returns SelfQrCodeNavArgs("handle", false) + } + + fun withQRCodeAssetUri(uri: String) = apply { + coEvery { qrAssetRepository.saveQRCode(any()) } returns uri } fun arrange() = this to SelfQRCodeViewModel( - savedStateHandle = savedStateHandle, - context = context, + selfQrCodeNavArgs = SelfQrCodeNavArgs("handle", false), selfUserId = TestUser.SELF_USER.id, selfServerLinks = selfServerConfig, - kaliumFileSystem = fakeKaliumFileSystem, - dispatchers = TestDispatcherProvider(), + qrAssetRepository = qrAssetRepository, analyticsManager = analyticsManager ) - - val fakeKaliumFileSystem: FakeKaliumFileSystem = FakeKaliumFileSystem() } } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt index 90bb34f4ea1..4a272ab91f8 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelTest.kt @@ -17,18 +17,16 @@ */ package com.wire.android.ui.userprofile.service -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri +import com.wire.android.framework.TestConversation +import com.wire.android.framework.TestConversationDetails import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.details.participants.usecase.ConversationRoleData import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveConversationRoleForUserUseCase -import com.ramcosta.composedestinations.generated.app.navArgs -import com.wire.android.framework.TestConversation -import com.wire.android.framework.TestConversationDetails import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.common.error.StorageFailure import com.wire.kalium.logic.data.conversation.Conversation @@ -55,7 +53,6 @@ import com.wire.kalium.logic.feature.service.ObserveIsServiceMemberUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf @@ -621,10 +618,11 @@ class ServiceDetailsViewModelTest { @MockK lateinit var addMemberToConversation: AddMemberToConversationUseCase - @MockK - lateinit var savedStateHandle: SavedStateHandle - private val selfUser = TestUser.SELF_USER + private var serviceDetailsNavArgs = ServiceDetailsNavArgs( + CONVERSATION_ID, + ServiceDetailsNavArgs.Id.BotServiceId(BOT_SERVICE) + ) private val viewModel by lazy { ServiceDetailsViewModel( @@ -640,7 +638,7 @@ class ServiceDetailsViewModelTest { removeMemberFromConversation, addServiceToConversation, addMemberToConversation, - savedStateHandle + serviceDetailsNavArgs ) } @@ -659,14 +657,14 @@ class ServiceDetailsViewModelTest { } fun withServiceBot(service: BotService, conversationId: ConversationId? = CONVERSATION_ID) = apply { - every { savedStateHandle.navArgs() } returns ServiceDetailsNavArgs( + serviceDetailsNavArgs = ServiceDetailsNavArgs( conversationId, ServiceDetailsNavArgs.Id.BotServiceId(service) ) } fun withServiceApp(service: UserId, conversationId: ConversationId? = CONVERSATION_ID) = apply { - every { savedStateHandle.navArgs() } returns ServiceDetailsNavArgs( + serviceDetailsNavArgs = ServiceDetailsNavArgs( conversationId, ServiceDetailsNavArgs.Id.AppId(service) ) diff --git a/build-logic/plugins/build.gradle.kts b/build-logic/plugins/build.gradle.kts index da308216ea4..8d6465be2fc 100644 --- a/build-logic/plugins/build.gradle.kts +++ b/build-logic/plugins/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { compileOnly(libs.android.gradlePlugin) compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.kover.gradlePlugin) + compileOnly(libs.metro.gradlePlugin) testImplementation(libs.junit4) testImplementation(kotlin("test")) diff --git a/build-logic/plugins/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 380fe40ba8f..7aa01ba2779 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -29,6 +29,7 @@ class AndroidApplicationConventionPlugin : Plugin { override fun apply(target: Project): Unit = with(target) { with(pluginManager) { apply("com.android.application") + apply("dev.zacsweers.metro") } extensions.configure { diff --git a/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt index f35a7bd3c84..d87aeb7bd7a 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -28,6 +28,7 @@ class AndroidLibraryConventionPlugin : Plugin { override fun apply(target: Project): Unit = with(target) { with(pluginManager) { apply("com.android.library") + apply("dev.zacsweers.metro") } extensions.configure { diff --git a/build-logic/plugins/src/main/kotlin/HiltConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/HiltConventionPlugin.kt index 2d1ce6e1710..778a037e829 100644 --- a/build-logic/plugins/src/main/kotlin/HiltConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/HiltConventionPlugin.kt @@ -28,12 +28,7 @@ class HiltConventionPlugin : Plugin { dependencies { add("implementation", findLibrary("hilt.android")) - add("androidTestImplementation", findLibrary("hilt.android")) - add("ksp", findLibrary("hilt.compiler")) - add("kspAndroidTest", findLibrary("hilt.compiler")) - - add("androidTestImplementation", findLibrary("hilt.test")) } } } diff --git a/build-logic/plugins/src/main/kotlin/KmpLibraryConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/KmpLibraryConventionPlugin.kt index 10fbc68841a..adcf914be43 100644 --- a/build-logic/plugins/src/main/kotlin/KmpLibraryConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/KmpLibraryConventionPlugin.kt @@ -28,7 +28,6 @@ class KmpLibraryConventionPlugin : Plugin { with(pluginManager) { apply("org.jetbrains.kotlin.multiplatform") apply("com.android.kotlin.multiplatform.library") - apply("org.jetbrains.kotlin.plugin.compose") } extensions.getByType().apply { @@ -44,7 +43,6 @@ class KmpLibraryConventionPlugin : Plugin { } } - iosX64() iosArm64() iosSimulatorArm64() } diff --git a/build.gradle.kts b/build.gradle.kts index d5fbf8590b1..da264968690 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -58,6 +58,7 @@ allprojects { plugins { id(ScriptPlugins.infrastructure) alias(libs.plugins.ksp) apply false // https://github.com/google/dagger/issues/3965 + alias(libs.plugins.metro) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.cyclonedx) } diff --git a/core/di/build.gradle.kts b/core/di/build.gradle.kts index 3aafaace2d4..9d94ebbddb2 100644 --- a/core/di/build.gradle.kts +++ b/core/di/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { implementation(libs.androidx.core) - implementation(libs.hilt.android) + implementation(libs.dagger) implementation(libs.compose.material3) + implementation(libs.androidx.lifecycle.viewModelCompose) } diff --git a/core/di/src/main/kotlin/com/wire/android/di/KaliumCoreLogic.kt b/core/di/src/main/kotlin/com/wire/android/di/KaliumCoreLogic.kt index f9d230ef7a0..d92c02c9c2c 100644 --- a/core/di/src/main/kotlin/com/wire/android/di/KaliumCoreLogic.kt +++ b/core/di/src/main/kotlin/com/wire/android/di/KaliumCoreLogic.kt @@ -22,3 +22,7 @@ import javax.inject.Qualifier @Qualifier @Retention(AnnotationRetention.BINARY) annotation class KaliumCoreLogic + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ApplicationContext diff --git a/core/di/src/main/kotlin/com/wire/android/di/metro/MetroViewModelGraph.kt b/core/di/src/main/kotlin/com/wire/android/di/metro/MetroViewModelGraph.kt new file mode 100644 index 00000000000..e5bf570015a --- /dev/null +++ b/core/di/src/main/kotlin/com/wire/android/di/metro/MetroViewModelGraph.kt @@ -0,0 +1,60 @@ +/* + * 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.di.metro + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory + +interface MetroViewModelGraph + +val LocalMetroViewModelGraph = staticCompositionLocalOf { + null +} + +@Composable +inline fun metroViewModel( + viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + }, + key: String? = null, + crossinline create: Graph.() -> VM, +): VM where Graph : MetroViewModelGraph, VM : ViewModel { + val graph = checkNotNull(LocalMetroViewModelGraph.current as? Graph) { + "No Metro graph matching ${Graph::class.qualifiedName} was provided" + } + val factory = remember(graph) { + viewModelFactory { + initializer { + graph.create() + } + } + } + return viewModel( + modelClass = VM::class, + viewModelStoreOwner = viewModelStoreOwner, + key = key, + factory = factory, + ) +} diff --git a/core/media/build.gradle.kts b/core/media/build.gradle.kts index 3aafaace2d4..31d5783a32f 100644 --- a/core/media/build.gradle.kts +++ b/core/media/build.gradle.kts @@ -6,6 +6,6 @@ plugins { dependencies { implementation(libs.androidx.core) - implementation(libs.hilt.android) + implementation(libs.dagger) implementation(libs.compose.material3) } diff --git a/core/media/src/main/kotlin/com/wire/android/media/PingRinger.kt b/core/media/src/main/kotlin/com/wire/android/media/PingRinger.kt index 1a210db3af3..3d8b658dba2 100644 --- a/core/media/src/main/kotlin/com/wire/android/media/PingRinger.kt +++ b/core/media/src/main/kotlin/com/wire/android/media/PingRinger.kt @@ -28,11 +28,12 @@ import android.os.Build import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject import javax.inject.Singleton @Singleton -class PingRinger @Inject constructor(private val context: Context) { +class PingRinger @Inject @MetroInject constructor(private val context: Context) { private var vibrator: Vibrator? = null diff --git a/core/navigation/src/main/kotlin/com/wire/android/navigation/wrapper/TabletDialogWrapper.kt b/core/navigation/src/main/kotlin/com/wire/android/navigation/wrapper/TabletDialogWrapper.kt index 264c883aa87..7527be1f524 100644 --- a/core/navigation/src/main/kotlin/com/wire/android/navigation/wrapper/TabletDialogWrapper.kt +++ b/core/navigation/src/main/kotlin/com/wire/android/navigation/wrapper/TabletDialogWrapper.kt @@ -25,23 +25,24 @@ import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.dp import com.ramcosta.composedestinations.scope.DestinationScope import com.ramcosta.composedestinations.spec.DestinationStyle import com.ramcosta.composedestinations.wrapper.DestinationWrapper -import com.wire.android.ui.common.dimensions -import com.wire.android.ui.theme.isTablet object TabletDialogWrapper : DestinationWrapper { @Composable override fun DestinationScope.Wrap(screenContent: @Composable () -> Unit) { val movableContent = remember(screenContent) { movableContentOf(screenContent) } + val isTablet = LocalConfiguration.current.smallestScreenWidthDp >= TABLET_MIN_SCREEN_WIDTH_DP val shouldWrapAsDialog = destination.style is DestinationStyle.Dialog || (isTablet && shouldWrapTabletRouteAsDialog(destination.route)) if (shouldWrapAsDialog) { Row( modifier = Modifier - .clip(RoundedCornerShape(dimensions().spacing20x)) + .clip(RoundedCornerShape(20.dp)) .imePadding() ) { movableContent() @@ -58,3 +59,5 @@ fun setTabletDialogRouteMatcher(routeMatcher: (String) -> Boolean) { @Volatile private var shouldWrapTabletRouteAsDialog: (String) -> Boolean = { false } + +private const val TABLET_MIN_SCREEN_WIDTH_DP = 600 diff --git a/core/notification/build.gradle.kts b/core/notification/build.gradle.kts index aff8001885d..286e46a517f 100644 --- a/core/notification/build.gradle.kts +++ b/core/notification/build.gradle.kts @@ -9,6 +9,6 @@ dependencies { implementation("com.wire.kalium:kalium-common") implementation("com.wire.kalium:kalium-data") implementation(libs.androidx.core) - implementation(libs.hilt.android) + implementation(libs.dagger) implementation(libs.compose.material3) } diff --git a/core/notification/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt b/core/notification/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt index 6aa0d551b7c..ece6c1e3eeb 100644 --- a/core/notification/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt +++ b/core/notification/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt @@ -30,11 +30,12 @@ import androidx.core.app.NotificationManagerCompat import com.wire.android.media.PingRinger import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.UserId +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject import javax.inject.Singleton @Singleton -class NotificationChannelsManager @Inject constructor( +class NotificationChannelsManager @Inject @MetroInject constructor( private val context: Context, private val notificationManagerCompat: NotificationManagerCompat ) { diff --git a/core/runtime-kmp/build.gradle.kts b/core/runtime-kmp/build.gradle.kts new file mode 100644 index 00000000000..9ecaf64e2b0 --- /dev/null +++ b/core/runtime-kmp/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id(libs.plugins.wire.kmp.library.get().pluginId) +} + +kotlin { + android { + namespace = "com.wire.android.runtime" + } + + sourceSets { + val commonMain by getting { + dependencies { + api(libs.coroutines.core) + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } +} diff --git a/core/runtime-kmp/src/commonMain/kotlin/com/wire/android/runtime/viewmodel/IosViewModel.kt b/core/runtime-kmp/src/commonMain/kotlin/com/wire/android/runtime/viewmodel/IosViewModel.kt new file mode 100644 index 00000000000..08aa3df40ce --- /dev/null +++ b/core/runtime-kmp/src/commonMain/kotlin/com/wire/android/runtime/viewmodel/IosViewModel.kt @@ -0,0 +1,32 @@ +/* + * 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.runtime.viewmodel + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +class IosViewModel( + val state: StateFlow, + val effects: Flow, + private val onIntent: (Intent) -> Unit, + private val onClose: () -> Unit = {}, +) { + fun sendIntent(intent: Intent) = onIntent(intent) + + fun close() = onClose() +} diff --git a/core/runtime-kmp/src/commonTest/kotlin/com/wire/android/runtime/viewmodel/IosViewModelTest.kt b/core/runtime-kmp/src/commonTest/kotlin/com/wire/android/runtime/viewmodel/IosViewModelTest.kt new file mode 100644 index 00000000000..d7f4150721f --- /dev/null +++ b/core/runtime-kmp/src/commonTest/kotlin/com/wire/android/runtime/viewmodel/IosViewModelTest.kt @@ -0,0 +1,60 @@ +/* + * 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.runtime.viewmodel + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class IosViewModelTest { + + @Test + fun givenIntentIsSent_whenSendIntentIsCalled_thenDelegateReceivesIntent() { + var receivedIntent: TestIntent? = null + val viewModel = IosViewModel( + state = MutableStateFlow(TestState), + effects = emptyFlow(), + onIntent = { receivedIntent = it }, + ) + + viewModel.sendIntent(TestIntent) + + assertEquals(TestIntent, receivedIntent) + } + + @Test + fun givenCloseCallback_whenCloseIsCalled_thenDelegateIsCalled() { + var closed = false + val viewModel = IosViewModel( + state = MutableStateFlow(TestState), + effects = emptyFlow(), + onIntent = {}, + onClose = { closed = true }, + ) + + viewModel.close() + + assertTrue(closed) + } + + private data object TestState + private data object TestEffect + private data object TestIntent +} diff --git a/core/ui-common-kmp/build.gradle.kts b/core/ui-common-kmp/build.gradle.kts index 23e38d8bfe0..b54a4d1510d 100644 --- a/core/ui-common-kmp/build.gradle.kts +++ b/core/ui-common-kmp/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id(libs.plugins.wire.kmp.library.get().pluginId) + alias(libs.plugins.compose.compiler) alias(libs.plugins.jetbrains.compose) } diff --git a/core/ui-common/build.gradle.kts b/core/ui-common/build.gradle.kts index 50762a6c69c..ba61474006c 100644 --- a/core/ui-common/build.gradle.kts +++ b/core/ui-common/build.gradle.kts @@ -4,7 +4,6 @@ plugins { alias(libs.plugins.kotlin.serialization) id(BuildPlugins.kotlinParcelize) id(BuildPlugins.junit5) - id(libs.plugins.wire.hilt.get().pluginId) alias(libs.plugins.compose.compiler) } @@ -14,6 +13,8 @@ android { } dependencies { + implementation(project(":core:di")) + implementation("com.wire.kalium:kalium-logic") implementation(libs.androidx.core) implementation(libs.androidx.appcompat) @@ -35,14 +36,10 @@ dependencies { implementation(libs.androidx.paging3) implementation(libs.androidx.paging3Compose) - // hilt - implementation(libs.hilt.navigationCompose) - implementation(libs.hilt.work) - // smaller view models implementation(libs.resaca.core) - implementation(libs.resaca.hilt) implementation(libs.bundlizer.core) + implementation(libs.dagger) // Compose Preview implementation(libs.compose.edgetoedge.preview) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/model/ImageAsset.kt b/core/ui-common/src/main/kotlin/com/wire/android/model/ImageAsset.kt index f506da2e277..2671bbd1fed 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/model/ImageAsset.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/model/ImageAsset.kt @@ -22,14 +22,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel +import com.wire.android.di.metro.MetroViewModelGraph +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.common.R import com.wire.android.util.ui.WireSessionImageLoader import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserAssetId -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -38,7 +38,6 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import okio.Path import okio.Path.Companion.toPath -import javax.inject.Inject @Stable @Serializable @@ -70,7 +69,7 @@ sealed class ImageAsset { withCrossfadeAnimation: Boolean = false ) = when { LocalInspectionMode.current -> painterResource(id = R.drawable.mock_image) - else -> hiltViewModel().imageLoader + else -> remoteAssetImageViewModel().imageLoader .paint(asset = this, fallbackData = fallbackData, withCrossfadeAnimation = withCrossfadeAnimation) } } @@ -111,5 +110,14 @@ object PathAsStringSerializer : KSerializer { override fun deserialize(decoder: Decoder): Path = decoder.decodeString().toPath(normalize = true) } -@HiltViewModel -class RemoteAssetImageViewModel @Inject constructor(val imageLoader: WireSessionImageLoader) : ViewModel() +interface ImageAssetViewModelGraph : MetroViewModelGraph { + val imageAssetViewModelFactory: ImageAssetViewModelFactory +} + +@Composable +private fun remoteAssetImageViewModel(): RemoteAssetImageViewModel = + metroViewModel { + imageAssetViewModelFactory.create() + } + +class RemoteAssetImageViewModel(val imageLoader: WireSessionImageLoader) : ViewModel() diff --git a/core/ui-common/src/main/kotlin/com/wire/android/model/ImageAssetViewModelFactory.kt b/core/ui-common/src/main/kotlin/com/wire/android/model/ImageAssetViewModelFactory.kt new file mode 100644 index 00000000000..dafe6cf293e --- /dev/null +++ b/core/ui-common/src/main/kotlin/com/wire/android/model/ImageAssetViewModelFactory.kt @@ -0,0 +1,29 @@ +/* + * 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.model + +import com.wire.android.util.ui.WireSessionImageLoader +import dev.zacsweers.metro.Inject + +@Inject +class ImageAssetViewModelFactory( + private val imageLoader: WireSessionImageLoader, +) { + fun create(): RemoteAssetImageViewModel = + RemoteAssetImageViewModel(imageLoader = imageLoader) +} diff --git a/core/ui-common/src/main/kotlin/com/wire/android/util/FileSizeFormatter.kt b/core/ui-common/src/main/kotlin/com/wire/android/util/FileSizeFormatter.kt index 1f2cb497455..d6661a178b5 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/util/FileSizeFormatter.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/util/FileSizeFormatter.kt @@ -19,8 +19,8 @@ package com.wire.android.util import android.content.Context import com.wire.android.ui.common.R -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject +import com.wire.android.di.ApplicationContext +import dev.zacsweers.metro.Inject class FileSizeFormatter @Inject constructor( @ApplicationContext private val context: Context diff --git a/features/cells/build.gradle.kts b/features/cells/build.gradle.kts index 9c466ee014f..d4d35fff5f3 100644 --- a/features/cells/build.gradle.kts +++ b/features/cells/build.gradle.kts @@ -1,7 +1,6 @@ plugins { id(libs.plugins.wire.android.library.get().pluginId) id(libs.plugins.wire.kover.get().pluginId) - id(libs.plugins.wire.hilt.get().pluginId) id(BuildPlugins.kotlinParcelize) id(BuildPlugins.junit5) alias(libs.plugins.ksp) @@ -14,6 +13,7 @@ dependencies { implementation("com.wire.kalium:kalium-common") implementation("com.wire.kalium:kalium-logic") implementation("com.wire.kalium:kalium-cells") + implementation(project(":core:di")) implementation(project(":core:ui-common")) implementation(libs.androidx.core) implementation(libs.androidx.appcompat) @@ -21,9 +21,7 @@ dependencies { implementation(libs.ktx.immutableCollections) implementation(libs.ktx.serialization) - // hilt - implementation(libs.hilt.navigationCompose) - implementation(libs.hilt.work) + implementation(libs.dagger) val composeBom = platform(libs.compose.bom) implementation(composeBom) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt index 3dc9c3ec67f..80b3901ec65 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/AllFilesScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.ramcosta.composedestinations.generated.cells.destinations.AddRemoveTagsScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkScreenDestination @@ -44,7 +43,9 @@ import com.wire.android.ui.common.topappbar.search.SearchTopBar fun AllFilesScreen( navigator: WireNavigator, modifier: Modifier = Modifier, - viewModel: CellViewModel = hiltViewModel(), + viewModel: CellViewModel = cellsMetroViewModel( + creationCallback = { cellViewModelFactory.create(CellFilesNavArgs(), null) } + ), ) { val pagingListItems = viewModel.nodesFlow.collectAsLazyPagingItems() diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/AndroidCellFileExternalActions.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/AndroidCellFileExternalActions.kt new file mode 100644 index 00000000000..18017079ad8 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/AndroidCellFileExternalActions.kt @@ -0,0 +1,67 @@ +/* + * 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.feature.cells.ui + +import com.wire.android.feature.cells.util.FileHelper +import dev.zacsweers.metro.Inject +import okio.Path.Companion.toPath + +class AndroidCellFileExternalActions @Inject constructor( + private val fileHelper: FileHelper, +) : CellFileExternalActions { + + override fun openLocalFile( + localPath: String, + assetName: String?, + mimeType: String, + onError: () -> Unit, + ) { + fileHelper.openAssetFileWithExternalApp( + localPath = localPath.toPath(), + assetName = assetName, + mimeType = mimeType, + onError = onError, + ) + } + + override fun openUrl( + url: String, + mimeType: String, + onError: () -> Unit, + ) { + fileHelper.openAssetUrlWithExternalApp( + url = url, + mimeType = mimeType, + onError = onError, + ) + } + + override fun shareLocalFile( + localPath: String, + assetName: String?, + mimeType: String, + onError: () -> Unit, + ) { + fileHelper.shareFileChooser( + assetDataPath = localPath.toPath(), + assetName = assetName, + mimeType = mimeType, + onError = onError, + ) + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt index c39c850c915..e76d3cec35c 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileActionsMenu.kt @@ -23,7 +23,7 @@ import com.wire.android.feature.cells.ui.model.OpenLoadState import com.wire.android.feature.cells.ui.model.isEditSupported import com.wire.android.feature.cells.ui.model.localFileAvailable import com.wire.kalium.logic.featureFlags.KaliumConfigs -import javax.inject.Inject +import dev.zacsweers.metro.Inject @Suppress("CyclomaticComplexMethod", "LongParameterList") class CellFileActionsMenu @Inject constructor( diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActions.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActions.kt new file mode 100644 index 00000000000..919dd40d831 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActions.kt @@ -0,0 +1,40 @@ +/* + * 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.feature.cells.ui + +interface CellFileExternalActions { + fun openLocalFile( + localPath: String, + assetName: String?, + mimeType: String, + onError: () -> Unit, + ) + + fun openUrl( + url: String, + mimeType: String, + onError: () -> Unit, + ) + + fun shareLocalFile( + localPath: String, + assetName: String?, + mimeType: String, + onError: () -> Unit, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileLocalPathCache.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileLocalPathCache.kt index 47bea39da4b..167b6392d7e 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileLocalPathCache.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileLocalPathCache.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update -import javax.inject.Inject +import dev.zacsweers.metro.Inject import javax.inject.Singleton /** diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt index 09d5a5ee328..cd6fad4c368 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFilesScreen.kt @@ -40,10 +40,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.lifecycle.ViewModel import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.model.CellNodeUi import com.wire.android.feature.cells.ui.util.PreviewMultipleThemes @@ -56,6 +58,15 @@ import com.wire.android.ui.common.progress.WireCircularProgressIndicator import com.wire.android.ui.common.typography import com.wire.android.ui.theme.WireTheme +@Composable +internal inline fun cellsMetroViewModel( + key: String? = null, + noinline creationCallback: CellViewModelGraph.() -> VM, +): VM = metroViewModel( + key = key, + create = creationCallback, +) + @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun CellFilesScreen( diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt index 5171fd7c61c..673714aa5e2 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModel.kt @@ -17,7 +17,7 @@ */ package com.wire.android.feature.cells.ui -import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.LoadState import androidx.paging.LoadStates @@ -25,8 +25,6 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.filter import androidx.paging.map -import com.ramcosta.composedestinations.generated.cells.destinations.ConversationFilesScreenDestination -import com.ramcosta.composedestinations.generated.cells.destinations.SearchScreenDestination import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.edit.OnlineEditor import com.wire.android.feature.cells.ui.model.CellNodeUi @@ -40,8 +38,8 @@ import com.wire.android.feature.cells.ui.search.DriveSearchScreenType import com.wire.android.feature.cells.ui.search.SearchNavArgs import com.wire.android.feature.cells.ui.search.sort.SortingCriteria import com.wire.android.feature.cells.ui.search.sort.toKaliumCriteria -import com.wire.android.feature.cells.util.FileHelper -import com.wire.android.ui.common.ActionsViewModel +import com.wire.android.ui.common.ActionsManager +import com.wire.android.ui.common.ActionsManagerImpl import com.wire.kalium.cells.data.FileFilters import com.wire.kalium.cells.data.SortingSpec import com.wire.kalium.cells.domain.model.Node @@ -55,7 +53,6 @@ import com.wire.kalium.common.functional.fold import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess import com.wire.kalium.logic.data.featureConfig.CollaboraEdition -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -73,33 +70,23 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import okio.Path.Companion.toPath -import javax.inject.Inject @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel -class CellViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class CellViewModel( + private val navArgs: CellFilesNavArgs, + private val searchNavArgs: SearchNavArgs?, private val getCellFilesPaged: GetPaginatedFilesFlowUseCase, private val deleteCellAsset: DeleteCellAssetUseCase, private val restoreNodeFromRecycleBinUseCase: RestoreNodeFromRecycleBinUseCase, private val isCellAvailable: IsAtLeastOneCellAvailableUseCase, - private val fileHelper: FileHelper, + private val fileExternalActions: CellFileExternalActions, private val getEditorUrl: GetEditorUrlUseCase, private val onlineEditor: OnlineEditor, private val cellFileActionsMenu: CellFileActionsMenu, private val getWireCellsConfig: GetWireCellConfigurationUseCase, private val sharedPathCache: CellFileLocalPathCache, private val openFileDownloadController: OpenFileDownloadController, -) : ActionsViewModel() { - - private val navArgs: CellFilesNavArgs = ConversationFilesScreenDestination.argsFrom(savedStateHandle) - private val searchNavArgs: SearchNavArgs? = try { - SearchScreenDestination.argsFrom(savedStateHandle) - } catch (_: RuntimeException) { - // Not coming from Search screen, ignore - null - } +) : ViewModel(), ActionsManager by ActionsManagerImpl() { // Show menu with actions for the selected file. private val _menu: MutableSharedFlow = MutableSharedFlow() @@ -310,7 +297,7 @@ class CellViewModel @Inject constructor( private fun openFileContentUrl(file: CellNodeUi.File) { file.contentUrl?.let { url -> - fileHelper.openAssetUrlWithExternalApp( + fileExternalActions.openUrl( url = url, mimeType = file.mimeType, onError = { @@ -322,8 +309,8 @@ class CellViewModel @Inject constructor( private fun openLocalFile(file: CellNodeUi.File) { file.localPath?.let { path -> - fileHelper.openAssetFileWithExternalApp( - localPath = path.toPath(), + fileExternalActions.openLocalFile( + localPath = path, assetName = file.name, mimeType = file.mimeType, onError = { @@ -380,8 +367,8 @@ class CellViewModel @Inject constructor( private fun shareFile(cell: CellNodeUi.File) { cell.localPath?.let { localPath -> - fileHelper.shareFileChooser( - assetDataPath = localPath.toPath(), + fileExternalActions.shareLocalFile( + localPath = localPath, assetName = cell.name, mimeType = cell.mimeType, onError = { sendAction(ShowError(CellError.OTHER_ERROR)) } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModelFactory.kt new file mode 100644 index 00000000000..d54d5a54cfe --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModelFactory.kt @@ -0,0 +1,64 @@ +/* + * 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.feature.cells.ui + +import com.wire.android.feature.cells.ui.edit.OnlineEditor +import com.wire.android.feature.cells.ui.search.SearchNavArgs +import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase +import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase +import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase +import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase +import com.wire.kalium.cells.domain.usecase.IsAtLeastOneCellAvailableUseCase +import com.wire.kalium.cells.domain.usecase.RestoreNodeFromRecycleBinUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class CellViewModelFactory( + private val getCellFilesPaged: GetPaginatedFilesFlowUseCase, + private val deleteCellAsset: DeleteCellAssetUseCase, + private val restoreNodeFromRecycleBinUseCase: RestoreNodeFromRecycleBinUseCase, + private val isCellAvailable: IsAtLeastOneCellAvailableUseCase, + private val fileExternalActions: CellFileExternalActions, + private val getEditorUrl: GetEditorUrlUseCase, + private val onlineEditor: OnlineEditor, + private val cellFileActionsMenu: CellFileActionsMenu, + private val getWireCellsConfig: GetWireCellConfigurationUseCase, + private val sharedPathCache: CellFileLocalPathCache, + private val openFileDownloadController: OpenFileDownloadController, +) { + fun create( + navArgs: CellFilesNavArgs, + searchNavArgs: SearchNavArgs?, + ): CellViewModel = + CellViewModel( + navArgs = navArgs, + searchNavArgs = searchNavArgs, + getCellFilesPaged = getCellFilesPaged, + deleteCellAsset = deleteCellAsset, + restoreNodeFromRecycleBinUseCase = restoreNodeFromRecycleBinUseCase, + isCellAvailable = isCellAvailable, + fileExternalActions = fileExternalActions, + getEditorUrl = getEditorUrl, + onlineEditor = onlineEditor, + cellFileActionsMenu = cellFileActionsMenu, + getWireCellsConfig = getWireCellsConfig, + sharedPathCache = sharedPathCache, + openFileDownloadController = openFileDownloadController, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModelGraph.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModelGraph.kt new file mode 100644 index 00000000000..b41f5083fea --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModelGraph.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.feature.cells.ui + +import com.wire.android.di.metro.MetroViewModelGraph +import com.wire.android.feature.cells.ui.create.file.CreateFileViewModelFactory +import com.wire.android.feature.cells.ui.create.folder.CreateFolderViewModelFactory +import com.wire.android.feature.cells.ui.movetofolder.MoveToFolderViewModelFactory +import com.wire.android.feature.cells.ui.publiclink.PublicLinkViewModelFactory +import com.wire.android.feature.cells.ui.publiclink.settings.expiration.PublicLinkExpirationScreenViewModelFactory +import com.wire.android.feature.cells.ui.publiclink.settings.password.PublicLinkPasswordScreenViewModelFactory +import com.wire.android.feature.cells.ui.rename.RenameNodeViewModelFactory +import com.wire.android.feature.cells.ui.search.SearchScreenViewModelFactory +import com.wire.android.feature.cells.ui.tags.AddRemoveTagsViewModelFactory +import com.wire.android.feature.cells.ui.versioning.VersionHistoryViewModelFactory + +interface CellViewModelGraph : MetroViewModelGraph { + val cellViewModelFactory: CellViewModelFactory + val createFileViewModelFactory: CreateFileViewModelFactory + val createFolderViewModelFactory: CreateFolderViewModelFactory + val renameNodeViewModelFactory: RenameNodeViewModelFactory + val moveToFolderViewModelFactory: MoveToFolderViewModelFactory + val addRemoveTagsViewModelFactory: AddRemoveTagsViewModelFactory + val versionHistoryViewModelFactory: VersionHistoryViewModelFactory + val publicLinkViewModelFactory: PublicLinkViewModelFactory + val publicLinkExpirationScreenViewModelFactory: PublicLinkExpirationScreenViewModelFactory + val publicLinkPasswordScreenViewModelFactory: PublicLinkPasswordScreenViewModelFactory + val searchScreenViewModelFactory: SearchScreenViewModelFactory +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt index 3cba2524e53..e05ad448d9b 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesScreen.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -101,7 +100,10 @@ import kotlinx.coroutines.flow.flowOf fun ConversationFilesScreen( navigator: WireNavigator, animatedVisibilityScope: AnimatedVisibilityScope, - viewModel: CellViewModel = hiltViewModel(), + args: CellFilesNavArgs, + viewModel: CellViewModel = cellsMetroViewModel( + creationCallback = { cellViewModelFactory.create(args, null) } + ), ) { ConversationFilesScreenContent( animatedVisibilityScope = animatedVisibilityScope, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt index 1bba1b1d535..24e1101bb70 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/ConversationFilesWithSlideInTransitionScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.ramcosta.composedestinations.generated.cells.destinations.RecycleBinScreenDestination import com.wire.android.feature.cells.R @@ -41,7 +40,9 @@ fun ConversationFilesWithSlideInTransitionScreen( navigator: WireNavigator, cellFilesNavArgs: CellFilesNavArgs, animatedVisibilityScope: AnimatedVisibilityScope, - viewModel: CellViewModel = hiltViewModel(), + viewModel: CellViewModel = cellsMetroViewModel( + creationCallback = { cellViewModelFactory.create(cellFilesNavArgs, null) } + ), ) { LaunchedEffect(viewModel.navigateToRecycleBinRoot.collectAsState().value) { if (viewModel.navigateToRecycleBinRoot.value) { diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/OpenFileDownloadController.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/OpenFileDownloadController.kt index 04b926590f0..c3328d81c14 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/OpenFileDownloadController.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/OpenFileDownloadController.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import okio.Path.Companion.toOkioPath -import javax.inject.Inject +import dev.zacsweers.metro.Inject /** * Controller responsible for managing the download and open flow for cell files. diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreen.kt index 79197ca7df3..0f4a25003d2 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreen.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.window.DialogProperties -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.common.FileNameError @@ -63,8 +63,11 @@ import java.util.Locale fun CreateFileScreen( navigator: WireNavigator, resultNavigator: ResultBackNavigator, + args: CreateFileScreenNavArgs, modifier: Modifier = Modifier, - createFileViewModel: CreateFileViewModel = hiltViewModel() + createFileViewModel: CreateFileViewModel = cellsMetroViewModel( + creationCallback = { createFileViewModelFactory.create(args) } + ) ) { val showErrorDialog = remember { mutableStateOf(false) } @@ -169,6 +172,7 @@ fun PreviewCreateFileScreen() { CreateFileScreen( navigator = PreviewNavigator, resultNavigator = PreviewResultBackNavigator as ResultBackNavigator, + args = CreateFileScreenNavArgs(uuid = "preview-uuid", fileType = FileType.DOCUMENT), ) } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModel.kt index dd41d6bee7d..d1b84a1e2f5 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModel.kt @@ -21,9 +21,7 @@ import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.cells.destinations.CreateFileScreenDestination import com.wire.android.feature.cells.ui.common.FileNameError import com.wire.android.feature.cells.ui.common.validateFileName import com.wire.android.ui.common.ActionsViewModel @@ -33,22 +31,17 @@ import com.wire.kalium.cells.domain.usecase.create.CreatePresentationFileUseCase import com.wire.kalium.cells.domain.usecase.create.CreateSpreadsheetFileUseCase import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CreateFileViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, +class CreateFileViewModel( + private val navArgs: CreateFileScreenNavArgs, private val createPresentationFileUseCase: CreatePresentationFileUseCase, private val createDocumentFileUseCase: CreateDocumentFileUseCase, private val createSpreadsheetFileUseCase: CreateSpreadsheetFileUseCase ) : ActionsViewModel() { - private val navArgs: CreateFileScreenNavArgs = CreateFileScreenDestination.argsFrom(savedStateHandle) - val fileExtension: String = navArgs.fileType.getExtension() val fileNameTextFieldState: TextFieldState = TextFieldState() diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModelFactory.kt new file mode 100644 index 00000000000..98f77249d21 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModelFactory.kt @@ -0,0 +1,38 @@ +/* + * 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.feature.cells.ui.create.file + +import com.wire.kalium.cells.domain.usecase.create.CreateDocumentFileUseCase +import com.wire.kalium.cells.domain.usecase.create.CreatePresentationFileUseCase +import com.wire.kalium.cells.domain.usecase.create.CreateSpreadsheetFileUseCase +import dev.zacsweers.metro.Inject + +@Inject +class CreateFileViewModelFactory( + private val createPresentationFileUseCase: CreatePresentationFileUseCase, + private val createDocumentFileUseCase: CreateDocumentFileUseCase, + private val createSpreadsheetFileUseCase: CreateSpreadsheetFileUseCase, +) { + fun create(args: CreateFileScreenNavArgs): CreateFileViewModel = + CreateFileViewModel( + navArgs = args, + createPresentationFileUseCase = createPresentationFileUseCase, + createDocumentFileUseCase = createDocumentFileUseCase, + createSpreadsheetFileUseCase = createSpreadsheetFileUseCase, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderScreen.kt index b6f27ad4318..11012f6c32a 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderScreen.kt @@ -33,7 +33,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.window.DialogProperties -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.common.FileNameError @@ -68,8 +68,12 @@ import java.util.Locale fun CreateFolderScreen( navigator: WireNavigator, resultNavigator: ResultBackNavigator, + args: CreateFolderScreenNavArgs, modifier: Modifier = Modifier, - createFolderViewModel: CreateFolderViewModel = hiltViewModel() + createFolderViewModel: CreateFolderViewModel = + cellsMetroViewModel( + creationCallback = { createFolderViewModelFactory.create(args) } + ) ) { val showErrorDialog = remember { mutableStateOf(false) } @@ -181,6 +185,7 @@ fun PreviewCreateFolderScreen() { CreateFolderScreen( navigator = PreviewNavigator, resultNavigator = PreviewResultBackNavigator as ResultBackNavigator, + args = CreateFolderScreenNavArgs(uuid = "preview-uuid"), ) } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModel.kt index c23d7321a70..c188f70cc5f 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModel.kt @@ -21,9 +21,7 @@ import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.cells.destinations.CreateFolderScreenDestination import com.wire.android.feature.cells.ui.common.FileNameError import com.wire.android.feature.cells.ui.common.validateFileName import com.wire.android.ui.common.ActionsViewModel @@ -31,20 +29,15 @@ import com.wire.android.ui.common.textfield.textAsFlow import com.wire.kalium.cells.domain.usecase.create.CreateFolderUseCase import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CreateFolderViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class CreateFolderViewModel( + private val navArgs: CreateFolderScreenNavArgs, private val createFolderUseCase: CreateFolderUseCase, ) : ActionsViewModel() { - private val navArgs: CreateFolderScreenNavArgs = CreateFolderScreenDestination.argsFrom(savedStateHandle) - val folderNameTextFieldState: TextFieldState = TextFieldState() var viewState: CreateFolderViewState by mutableStateOf(CreateFolderViewState()) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModelFactory.kt new file mode 100644 index 00000000000..8d1d38a9e91 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModelFactory.kt @@ -0,0 +1,32 @@ +/* + * 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.feature.cells.ui.create.folder + +import com.wire.kalium.cells.domain.usecase.create.CreateFolderUseCase +import dev.zacsweers.metro.Inject + +@Inject +class CreateFolderViewModelFactory( + private val createFolderUseCase: CreateFolderUseCase, +) { + fun create(args: CreateFolderScreenNavArgs): CreateFolderViewModel = + CreateFolderViewModel( + navArgs = args, + createFolderUseCase = createFolderUseCase, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/edit/OnlineEditor.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/edit/OnlineEditor.kt index 5ebc1edf1b1..f70ec9dd381 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/edit/OnlineEditor.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/edit/OnlineEditor.kt @@ -27,7 +27,7 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_SYSTEM -import javax.inject.Inject +import dev.zacsweers.metro.Inject class OnlineEditor @Inject constructor( private val context: Context, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderScreen.kt index 4dfaf1a41af..81447a54283 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderScreen.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import com.wire.android.feature.cells.R @@ -78,8 +78,12 @@ import com.wire.android.ui.theme.wireTypography fun MoveToFolderScreen( navigator: WireNavigator, createFolderResultRecipient: ResultRecipient, + args: MoveToFolderNavArgs, modifier: Modifier = Modifier, - moveToFolderViewModel: MoveToFolderViewModel = hiltViewModel() + moveToFolderViewModel: MoveToFolderViewModel = + cellsMetroViewModel( + creationCallback = { moveToFolderViewModelFactory.create(args) } + ) ) { val context = LocalContext.current val viewState by moveToFolderViewModel.state.collectAsState() @@ -292,6 +296,11 @@ fun PreviewMoveToFolderScreen() { WireTheme { MoveToFolderScreen( navigator = PreviewNavigator, + args = MoveToFolderNavArgs( + currentPath = "", + nodeToMovePath = "", + uuid = "" + ), createFolderResultRecipient = PreviewResultRecipient as ResultRecipient ) } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModel.kt index d977a18054d..adf206f5267 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModel.kt @@ -17,9 +17,7 @@ */ package com.wire.android.feature.cells.ui.movetofolder -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.cells.destinations.MoveToFolderScreenDestination import com.wire.android.feature.cells.ui.model.CellNodeUi import com.wire.android.feature.cells.ui.model.toUiModel import com.wire.android.ui.common.ActionsViewModel @@ -27,22 +25,17 @@ import com.wire.kalium.cells.domain.usecase.GetFoldersUseCase import com.wire.kalium.cells.domain.usecase.MoveNodeUseCase import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class MoveToFolderViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class MoveToFolderViewModel( + private val navArgs: MoveToFolderNavArgs, private val getFoldersUseCase: GetFoldersUseCase, private val moveNodeUseCase: MoveNodeUseCase ) : ActionsViewModel() { - private val navArgs: MoveToFolderNavArgs = MoveToFolderScreenDestination.argsFrom(savedStateHandle) - private val currentPath: String = navArgs.currentPath private val nodeToMovePath: String = navArgs.nodeToMovePath private val nodeUuid: String = navArgs.uuid diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModelFactory.kt new file mode 100644 index 00000000000..c254455de9c --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModelFactory.kt @@ -0,0 +1,35 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.cells.ui.movetofolder + +import com.wire.kalium.cells.domain.usecase.GetFoldersUseCase +import com.wire.kalium.cells.domain.usecase.MoveNodeUseCase +import dev.zacsweers.metro.Inject + +@Inject +class MoveToFolderViewModelFactory( + private val getFoldersUseCase: GetFoldersUseCase, + private val moveNodeUseCase: MoveNodeUseCase, +) { + fun create(args: MoveToFolderNavArgs): MoveToFolderViewModel = + MoveToFolderViewModel( + navArgs = args, + getFoldersUseCase = getFoldersUseCase, + moveNodeUseCase = moveNodeUseCase, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkScreen.kt index f61139b62a2..082b7b94c7a 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkScreen.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import com.ramcosta.composedestinations.spec.TypedDestinationSpec @@ -81,8 +81,11 @@ fun PublicLinkScreen( navigator: WireNavigator, onPasswordChange: ResultRecipient, onExpirationChange: ResultRecipient, + args: PublicLinkNavArgs, modifier: Modifier = Modifier, - viewModel: PublicLinkViewModel = hiltViewModel(), + viewModel: PublicLinkViewModel = cellsMetroViewModel( + creationCallback = { publicLinkViewModelFactory.create(args) } + ), ) { val context = LocalContext.current diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModel.kt index d2fe59ce6ae..8785f02a038 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModel.kt @@ -17,9 +17,7 @@ */ package com.wire.android.feature.cells.ui.publiclink -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkScreenDestination import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.publiclink.settings.expiration.PublicLinkExpirationResult import com.wire.android.feature.cells.util.FileHelper @@ -30,24 +28,19 @@ import com.wire.kalium.cells.domain.usecase.publiclink.DeletePublicLinkUseCase import com.wire.kalium.cells.domain.usecase.publiclink.GetPublicLinkUseCase import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class PublicLinkViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class PublicLinkViewModel( + private val navArgs: PublicLinkNavArgs, private val createPublicLink: CreatePublicLinkUseCase, private val getPublicLinkUseCase: GetPublicLinkUseCase, private val deletePublicLinkUseCase: DeletePublicLinkUseCase, private val fileHelper: FileHelper, ) : ActionsViewModel() { - private val navArgs: PublicLinkNavArgs = PublicLinkScreenDestination.argsFrom(savedStateHandle) - private val _state = MutableStateFlow( PublicLinkViewState( isEnabled = navArgs.publicLinkId != null, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModelFactory.kt new file mode 100644 index 00000000000..87dff4a5d9f --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModelFactory.kt @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.cells.ui.publiclink + +import com.wire.android.feature.cells.util.FileHelper +import com.wire.kalium.cells.domain.usecase.publiclink.CreatePublicLinkUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.DeletePublicLinkUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.GetPublicLinkUseCase +import dev.zacsweers.metro.Inject + +@Inject +class PublicLinkViewModelFactory( + private val createPublicLink: CreatePublicLinkUseCase, + private val getPublicLinkUseCase: GetPublicLinkUseCase, + private val deletePublicLinkUseCase: DeletePublicLinkUseCase, + private val fileHelper: FileHelper, +) { + fun create(navArgs: PublicLinkNavArgs): PublicLinkViewModel = + PublicLinkViewModel( + navArgs = navArgs, + createPublicLink = createPublicLink, + getPublicLinkUseCase = getPublicLinkUseCase, + deletePublicLinkUseCase = deletePublicLinkUseCase, + fileHelper = fileHelper, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreen.kt index e2e528607ab..cc8bdf6ac96 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreen.kt @@ -39,7 +39,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.common.WireCellErrorDialog @@ -71,8 +71,12 @@ import kotlinx.parcelize.Parcelize @Composable internal fun PublicLinkExpirationScreen( resultNavigator: ResultBackNavigator, + args: PublicLinkExpirationScreenNavArgs, modifier: Modifier = Modifier, - viewModel: PublicLinkExpirationScreenViewModel = hiltViewModel(), + viewModel: PublicLinkExpirationScreenViewModel = + cellsMetroViewModel( + creationCallback = { publicLinkExpirationScreenViewModelFactory.create(args) } + ), ) { val state by viewModel.state.collectAsState() diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModel.kt index 47453f7be99..92e043b4968 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModel.kt @@ -17,9 +17,7 @@ */ package com.wire.android.feature.cells.ui.publiclink.settings.expiration -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkExpirationScreenDestination import com.wire.android.feature.cells.R import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.common.datetime.TimePickerResult @@ -29,7 +27,6 @@ import com.wire.android.util.uiLinkExpirationTime import com.wire.kalium.cells.domain.usecase.publiclink.SetPublicLinkExpirationUseCase import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -41,16 +38,12 @@ import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime -import javax.inject.Inject -@HiltViewModel -internal class PublicLinkExpirationScreenViewModel @Inject constructor( +internal class PublicLinkExpirationScreenViewModel( + private val navArgs: PublicLinkExpirationScreenNavArgs, val setExpiration: SetPublicLinkExpirationUseCase, - val savedStateHandle: SavedStateHandle, ) : ActionsViewModel() { - private val navArgs: PublicLinkExpirationScreenNavArgs = PublicLinkExpirationScreenDestination.argsFrom(savedStateHandle) - var isExpirationSet: Boolean = navArgs.expiresAt != null private set diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModelFactory.kt new file mode 100644 index 00000000000..807cd3e678e --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModelFactory.kt @@ -0,0 +1,32 @@ +/* + * 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.feature.cells.ui.publiclink.settings.expiration + +import com.wire.kalium.cells.domain.usecase.publiclink.SetPublicLinkExpirationUseCase +import dev.zacsweers.metro.Inject + +@Inject +class PublicLinkExpirationScreenViewModelFactory( + private val setExpiration: SetPublicLinkExpirationUseCase, +) { + internal fun create(navArgs: PublicLinkExpirationScreenNavArgs): PublicLinkExpirationScreenViewModel = + PublicLinkExpirationScreenViewModel( + navArgs = navArgs, + setExpiration = setExpiration, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreen.kt index cdd8845c695..405216b0609 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreen.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.common.WireCellErrorDialog @@ -68,8 +68,12 @@ import com.wire.android.ui.theme.WireTheme @Composable internal fun PublicLinkPasswordScreen( resultNavigator: ResultBackNavigator, + args: PublicLinkPasswordNavArgs, modifier: Modifier = Modifier, - viewModel: PublicLinkPasswordScreenViewModel = hiltViewModel(), + viewModel: PublicLinkPasswordScreenViewModel = + cellsMetroViewModel( + creationCallback = { publicLinkPasswordScreenViewModelFactory.create(args) } + ), ) { val state by viewModel.state.collectAsState() val clipboardManager = LocalClipboardManager.current diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModel.kt index 6f5dce0b87c..d668477ec6c 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModel.kt @@ -20,9 +20,7 @@ package com.wire.android.feature.cells.ui.publiclink.settings.password import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkPasswordScreenDestination import com.wire.android.feature.cells.R import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.common.textfield.textAsFlow @@ -32,25 +30,20 @@ import com.wire.kalium.cells.domain.usecase.publiclink.UpdatePublicLinkPasswordU import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess import com.wire.kalium.logic.util.RandomPassword -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -internal class PublicLinkPasswordScreenViewModel @Inject constructor( +internal class PublicLinkPasswordScreenViewModel( + private val navArgs: PublicLinkPasswordNavArgs, private val generateRandomPassword: RandomPassword, private val createPassword: CreatePublicLinkPasswordUseCase, private val updatePassword: UpdatePublicLinkPasswordUseCase, private val getPublicLinkPassword: GetPublicLinkPasswordUseCase, - val savedStateHandle: SavedStateHandle, ) : ActionsViewModel() { - private val navArgs: PublicLinkPasswordNavArgs = PublicLinkPasswordScreenDestination.argsFrom(savedStateHandle) - internal var isPasswordCreated = navArgs.passwordEnabled private set diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModelFactory.kt new file mode 100644 index 00000000000..878797cce1a --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModelFactory.kt @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.cells.ui.publiclink.settings.password + +import com.wire.kalium.cells.domain.usecase.publiclink.CreatePublicLinkPasswordUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.GetPublicLinkPasswordUseCase +import com.wire.kalium.cells.domain.usecase.publiclink.UpdatePublicLinkPasswordUseCase +import com.wire.kalium.logic.util.RandomPassword +import dev.zacsweers.metro.Inject + +@Inject +class PublicLinkPasswordScreenViewModelFactory( + private val generateRandomPassword: RandomPassword, + private val createPassword: CreatePublicLinkPasswordUseCase, + private val updatePassword: UpdatePublicLinkPasswordUseCase, + private val getPublicLinkPassword: GetPublicLinkPasswordUseCase, +) { + internal fun create(navArgs: PublicLinkPasswordNavArgs): PublicLinkPasswordScreenViewModel = + PublicLinkPasswordScreenViewModel( + navArgs = navArgs, + generateRandomPassword = generateRandomPassword, + createPassword = createPassword, + updatePassword = updatePassword, + getPublicLinkPassword = getPublicLinkPassword, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/recyclebin/RecycleBinScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/recyclebin/RecycleBinScreen.kt index 0f0d45e3ed8..b03db95653b 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/recyclebin/RecycleBinScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/recyclebin/RecycleBinScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.CellFilesNavArgs @@ -54,8 +54,11 @@ import com.wire.android.ui.theme.wireTypography @Composable fun RecycleBinScreen( navigator: WireNavigator, + args: CellFilesNavArgs, modifier: Modifier = Modifier, - cellViewModel: CellViewModel = hiltViewModel() + cellViewModel: CellViewModel = cellsMetroViewModel( + creationCallback = { cellViewModelFactory.create(args, null) } + ) ) { Box(modifier = modifier) { diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeScreen.kt index eb6a5463321..c0e5328bfb9 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeScreen.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.common.FILE_NAME_MAX_COUNT import com.wire.android.feature.cells.ui.common.FileNameError @@ -64,8 +64,11 @@ import com.wire.android.ui.theme.wireDimensions @Composable fun RenameNodeScreen( navigator: WireNavigator, + args: RenameNodeNavArgs, modifier: Modifier = Modifier, - renameNodeViewModel: RenameNodeViewModel = hiltViewModel() + renameNodeViewModel: RenameNodeViewModel = cellsMetroViewModel( + creationCallback = { renameNodeViewModelFactory.create(args) } + ) ) { val context = LocalContext.current @@ -170,7 +173,8 @@ private fun computeNameErrorState(error: FileNameError?, isFolder: Boolean): Wir fun PreviewRenameNodeScreen() { WireTheme { RenameNodeScreen( - navigator = PreviewNavigator + navigator = PreviewNavigator, + args = RenameNodeNavArgs() ) } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeViewModel.kt index 779c359c0b1..08fcffb816f 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeViewModel.kt @@ -21,9 +21,7 @@ import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.cells.destinations.RenameNodeScreenDestination import com.wire.android.feature.cells.ui.common.FileNameError import com.wire.android.feature.cells.ui.common.validateFileName import com.wire.android.ui.common.ActionsViewModel @@ -33,23 +31,18 @@ import com.wire.kalium.cells.domain.usecase.RenameNodeUseCase import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess import com.wire.kalium.logic.util.splitFileExtension -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject import kotlin.time.Duration.Companion.seconds -@HiltViewModel -class RenameNodeViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class RenameNodeViewModel( + private val navArgs: RenameNodeNavArgs, private val renameNodeUseCase: RenameNodeUseCase, ) : ActionsViewModel() { - private val navArgs: RenameNodeNavArgs = RenameNodeScreenDestination.argsFrom(savedStateHandle) - private var clearErrorJob: Job? = null fun isFolder(): Boolean = navArgs.isFolder ?: false diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeViewModelFactory.kt new file mode 100644 index 00000000000..427fd02ff16 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeViewModelFactory.kt @@ -0,0 +1,32 @@ +/* + * 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.feature.cells.ui.rename + +import com.wire.kalium.cells.domain.usecase.RenameNodeUseCase +import dev.zacsweers.metro.Inject + +@Inject +class RenameNodeViewModelFactory( + private val renameNodeUseCase: RenameNodeUseCase, +) { + fun create(args: RenameNodeNavArgs): RenameNodeViewModel = + RenameNodeViewModel( + navArgs = args, + renameNodeUseCase = renameNodeUseCase, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt index fcdbd4af8a4..08fee490137 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.collectAsLazyPagingItems import com.ramcosta.composedestinations.generated.cells.destinations.AddRemoveTagsScreenDestination @@ -75,8 +75,12 @@ fun SearchScreen( navigator: WireNavigator, animatedVisibilityScope: AnimatedVisibilityScope, cellViewModel: CellViewModel, + args: SearchNavArgs, modifier: Modifier = Modifier, - searchScreenViewModel: SearchScreenViewModel = hiltViewModel(), + searchScreenViewModel: SearchScreenViewModel = + cellsMetroViewModel( + creationCallback = { searchScreenViewModelFactory.create(args) } + ), ) { val uiState by searchScreenViewModel.uiState.collectAsStateWithLifecycle() diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt index 321b827f6c3..42d06142013 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModel.kt @@ -17,7 +17,6 @@ */ package com.wire.android.feature.cells.ui.search -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.LoadState @@ -25,7 +24,6 @@ import androidx.paging.LoadStates import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map -import com.ramcosta.composedestinations.generated.cells.destinations.SearchScreenDestination import com.wire.android.feature.cells.ui.CellFileLocalPathCache import com.wire.android.feature.cells.ui.model.CellNodeUi import com.wire.android.feature.cells.ui.model.toUiModel @@ -51,7 +49,6 @@ import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase import com.wire.kalium.common.functional.onSuccess import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.user.UserAssetId -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -65,14 +62,12 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject private const val SEARCH_DEBOUNCE_MILLIS = 200L @Suppress("TooManyFunctions") -@HiltViewModel -class SearchScreenViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class SearchScreenViewModel( + private val navArgs: SearchNavArgs, private val getAllTagsUseCase: GetAllTagsUseCase, private val getCellFilesPaged: GetPaginatedFilesFlowUseCase, private val getOwners: GetOwnersUseCase, @@ -90,8 +85,6 @@ class SearchScreenViewModel @Inject constructor( val conversationId: String?, ) - private val navArgs: SearchNavArgs = SearchScreenDestination.argsFrom(savedStateHandle) - val screenType = navArgs.screenType val parentRoute = navArgs.parentRoute diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModelFactory.kt new file mode 100644 index 00000000000..a9c9067a720 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModelFactory.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.feature.cells.ui.search + +import com.wire.android.feature.cells.ui.CellFileLocalPathCache +import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase +import com.wire.kalium.cells.domain.usecase.GetOwnersUseCase +import com.wire.kalium.cells.domain.usecase.GetPaginatedCellConversationsFlowUseCase +import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase +import dev.zacsweers.metro.Inject + +@Inject +class SearchScreenViewModelFactory( + private val getAllTagsUseCase: GetAllTagsUseCase, + private val getCellFilesPaged: GetPaginatedFilesFlowUseCase, + private val getOwners: GetOwnersUseCase, + private val getPaginatedConversations: GetPaginatedCellConversationsFlowUseCase, + private val sharedPathCache: CellFileLocalPathCache, +) { + fun create(navArgs: SearchNavArgs): SearchScreenViewModel = + SearchScreenViewModel( + navArgs = navArgs, + getAllTagsUseCase = getAllTagsUseCase, + getCellFilesPaged = getCellFilesPaged, + getOwners = getOwners, + getPaginatedConversations = getPaginatedConversations, + sharedPathCache = sharedPathCache, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsScreen.kt index 7b5c4710cdc..022bf232dee 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsScreen.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import com.wire.android.feature.cells.R import com.wire.android.model.ClickBlockParams import com.wire.android.navigation.WireNavigator @@ -77,8 +77,12 @@ import kotlinx.coroutines.launch @Composable fun AddRemoveTagsScreen( navigator: WireNavigator, + args: AddRemoveTagsNavArgs, modifier: Modifier = Modifier, - addRemoveTagsViewModel: AddRemoveTagsViewModel = hiltViewModel(), + addRemoveTagsViewModel: AddRemoveTagsViewModel = + cellsMetroViewModel( + creationCallback = { addRemoveTagsViewModelFactory.create(args) } + ), ) { val context = LocalContext.current diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModel.kt index fa6ce40bcb0..cb573590155 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModel.kt @@ -20,33 +20,27 @@ package com.wire.android.feature.cells.ui.tags import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.runtime.snapshotFlow -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.cells.destinations.AddRemoveTagsScreenDestination import com.wire.android.ui.common.ActionsViewModel import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase import com.wire.kalium.cells.domain.usecase.RemoveNodeTagsUseCase import com.wire.kalium.cells.domain.usecase.UpdateNodeTagsUseCase import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class AddRemoveTagsViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class AddRemoveTagsViewModel( + private val navArgs: AddRemoveTagsNavArgs, private val getAllTagsUseCase: GetAllTagsUseCase, private val updateNodeTagsUseCase: UpdateNodeTagsUseCase, private val removeNodeTagsUseCase: RemoveNodeTagsUseCase, ) : ActionsViewModel() { - private val navArgs: AddRemoveTagsNavArgs = AddRemoveTagsScreenDestination.argsFrom(savedStateHandle) private val initialTags: Set = navArgs.tags.toSet() private val disallowedChars = setOf(",", ";", "/", "\\", "\"", "\'", "<", ">") diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModelFactory.kt new file mode 100644 index 00000000000..a0c904814c2 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModelFactory.kt @@ -0,0 +1,38 @@ +/* + * 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.feature.cells.ui.tags + +import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase +import com.wire.kalium.cells.domain.usecase.RemoveNodeTagsUseCase +import com.wire.kalium.cells.domain.usecase.UpdateNodeTagsUseCase +import dev.zacsweers.metro.Inject + +@Inject +class AddRemoveTagsViewModelFactory( + private val getAllTagsUseCase: GetAllTagsUseCase, + private val updateNodeTagsUseCase: UpdateNodeTagsUseCase, + private val removeNodeTagsUseCase: RemoveNodeTagsUseCase, +) { + fun create(args: AddRemoveTagsNavArgs): AddRemoveTagsViewModel = + AddRemoveTagsViewModel( + navArgs = args, + getAllTagsUseCase = getAllTagsUseCase, + updateNodeTagsUseCase = updateNodeTagsUseCase, + removeNodeTagsUseCase = removeNodeTagsUseCase, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryScreen.kt index 03cbe043c6d..72277c5fc0d 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryScreen.kt @@ -38,7 +38,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel +import com.wire.android.feature.cells.ui.cellsMetroViewModel import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.common.ErrorScreen import com.wire.android.feature.cells.ui.common.LoadingScreen @@ -71,8 +71,12 @@ import kotlinx.coroutines.launch @Composable fun VersionHistoryScreen( navigator: WireNavigator, + args: VersionHistoryNavArgs, modifier: Modifier = Modifier, - versionHistoryViewModel: VersionHistoryViewModel = hiltViewModel() + versionHistoryViewModel: VersionHistoryViewModel = + cellsMetroViewModel( + creationCallback = { versionHistoryViewModelFactory.create(args) } + ) ) { val optionsBottomSheetState = rememberWireModalSheetState>() val snackbarHostState = LocalSnackbarHostState.current diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModel.kt index 88dca59b603..5d0519f8b3b 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModel.kt @@ -19,10 +19,8 @@ package com.wire.android.feature.cells.ui.versioning import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.cells.destinations.VersionHistoryScreenDestination import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.edit.OnlineEditor import com.wire.android.feature.cells.ui.versioning.download.DownloadState @@ -41,7 +39,6 @@ import com.wire.kalium.cells.domain.usecase.versioning.GetNodeVersionsUseCase import com.wire.kalium.cells.domain.usecase.versioning.RestoreNodeVersionUseCase import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -50,11 +47,9 @@ import java.time.Instant import java.time.LocalDate import java.time.ZoneId import java.time.format.DateTimeFormatter -import javax.inject.Inject -@HiltViewModel -class VersionHistoryViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, +class VersionHistoryViewModel( + private val navArgs: VersionHistoryNavArgs, private val getNodeVersionsUseCase: GetNodeVersionsUseCase, private val fileSizeFormatter: FileSizeFormatter, private val restoreNodeVersionUseCase: RestoreNodeVersionUseCase, @@ -64,9 +59,6 @@ class VersionHistoryViewModel @Inject constructor( private val getEditorUrl: GetEditorUrlUseCase, private val dispatchers: DispatcherProvider, ) : ViewModel() { - - private val navArgs: VersionHistoryNavArgs = VersionHistoryScreenDestination.argsFrom(savedStateHandle) - val fileName = navArgs.fileName var versionHistoryState: MutableState = mutableStateOf(VersionHistoryState.Idle) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelFactory.kt new file mode 100644 index 00000000000..26a05259979 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelFactory.kt @@ -0,0 +1,53 @@ +/* + * 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.feature.cells.ui.versioning + +import com.wire.android.feature.cells.ui.edit.OnlineEditor +import com.wire.android.feature.cells.util.FileHelper +import com.wire.android.util.FileSizeFormatter +import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase +import com.wire.kalium.cells.domain.usecase.download.DownloadCellVersionUseCase +import com.wire.kalium.cells.domain.usecase.versioning.GetNodeVersionsUseCase +import com.wire.kalium.cells.domain.usecase.versioning.RestoreNodeVersionUseCase +import dev.zacsweers.metro.Inject + +@Inject +class VersionHistoryViewModelFactory( + private val getNodeVersionsUseCase: GetNodeVersionsUseCase, + private val fileSizeFormatter: FileSizeFormatter, + private val restoreNodeVersionUseCase: RestoreNodeVersionUseCase, + private val downloadCellVersionUseCase: DownloadCellVersionUseCase, + private val fileHelper: FileHelper, + private val onlineEditor: OnlineEditor, + private val getEditorUrl: GetEditorUrlUseCase, + private val dispatchers: DispatcherProvider, +) { + fun create(navArgs: VersionHistoryNavArgs): VersionHistoryViewModel = + VersionHistoryViewModel( + navArgs = navArgs, + getNodeVersionsUseCase = getNodeVersionsUseCase, + fileSizeFormatter = fileSizeFormatter, + restoreNodeVersionUseCase = restoreNodeVersionUseCase, + downloadCellVersionUseCase = downloadCellVersionUseCase, + fileHelper = fileHelper, + onlineEditor = onlineEditor, + getEditorUrl = getEditorUrl, + dispatchers = dispatchers, + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/util/FileHelper.kt b/features/cells/src/main/java/com/wire/android/feature/cells/util/FileHelper.kt index 8d6dfa14980..a08a3693782 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/util/FileHelper.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/util/FileHelper.kt @@ -26,12 +26,12 @@ import android.os.Environment import android.provider.MediaStore import android.os.Build import androidx.core.content.FileProvider -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext +import dev.zacsweers.metro.Inject import okio.Path import java.io.File import java.io.FileOutputStream import java.io.OutputStream -import javax.inject.Inject class FileHelper @Inject constructor( @ApplicationContext private val context: Context diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/util/FileNameResolver.kt b/features/cells/src/main/java/com/wire/android/feature/cells/util/FileNameResolver.kt index e4236641961..e4d62208ca4 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/util/FileNameResolver.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/util/FileNameResolver.kt @@ -18,7 +18,7 @@ package com.wire.android.feature.cells.util import java.io.File -import javax.inject.Inject +import dev.zacsweers.metro.Inject class FileNameResolver @Inject constructor() { /** diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/CellViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/CellViewModelTest.kt index ba82692c7ee..c00e628776d 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/CellViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/CellViewModelTest.kt @@ -17,15 +17,14 @@ */ package com.wire.android.feature.cells.ui -import androidx.lifecycle.SavedStateHandle import androidx.paging.LoadState import androidx.paging.LoadStates import androidx.paging.PagingData import androidx.paging.testing.asSnapshot import app.cash.turbine.test -import com.ramcosta.composedestinations.generated.cells.destinations.ConversationFilesScreenDestination -import com.wire.android.config.NavigationTestExtension import com.wire.android.feature.cells.ui.edit.OnlineEditor +import com.wire.android.feature.cells.ui.model.CellNodeUi +import com.wire.android.feature.cells.ui.model.NodeBottomSheetAction import com.wire.android.feature.cells.ui.model.OpenLoadState import com.wire.android.feature.cells.ui.model.toUiModel import com.wire.android.feature.cells.util.FileHelper @@ -44,7 +43,6 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.mockkObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOf @@ -53,16 +51,13 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import okio.Path.Companion.toPath import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith import java.io.File -@ExtendWith(NavigationTestExtension::class) class CellViewModelTest { private companion object { @@ -90,7 +85,6 @@ class CellViewModelTest { modifiedTime = 1234567890L, ) ) - val localFilePath = "localPath".toPath() } private val dispatcher = UnconfinedTestDispatcher() @@ -125,7 +119,7 @@ class CellViewModelTest { viewModel.sendIntent(CellViewIntent.OnItemClick(testFiles[0].toUiModel())) - coVerify(exactly = 1) { arrangement.fileHelper.openAssetFileWithExternalApp(any(), any(), any(), any()) } + coVerify(exactly = 1) { arrangement.fileExternalActions.openLocalFile(any(), any(), any(), any()) } } @Test @@ -141,7 +135,7 @@ class CellViewModelTest { viewModel.sendIntent(CellViewIntent.OnItemClick(testFile.toUiModel())) - coVerify(exactly = 1) { arrangement.fileHelper.openAssetUrlWithExternalApp(any(), any(), any()) } + coVerify(exactly = 1) { arrangement.fileExternalActions.openUrl(any(), any(), any()) } } @Test @@ -164,6 +158,26 @@ class CellViewModelTest { coVerify(exactly = 1) { arrangement.downloadCellFileUseCase(any(), any(), any(), any(), any()) } } + @Test + fun `given share action selected for local file then file is shared`() = runTest { + val testFile = testFiles[0].toUiModel() + val (arrangement, viewModel) = Arrangement() + .withLoadSuccess() + .withShareActionResult(testFile) + .arrange() + + viewModel.sendIntent(CellViewIntent.OnMenuItemActionSelected(testFile, NodeBottomSheetAction.SHARE)) + + coVerify(exactly = 1) { + arrangement.fileExternalActions.shareLocalFile( + localPath = testFile.localPath!!, + assetName = testFile.name, + mimeType = testFile.mimeType, + onError = any(), + ) + } + } + @Test fun `given file has local path in DB when clicked with error state then file opened without re-downloading`() = runTest { val (arrangement, viewModel) = Arrangement() @@ -178,7 +192,7 @@ class CellViewModelTest { advanceUntilIdle() coVerify(exactly = 0) { arrangement.downloadCellFileUseCase(any(), any(), any(), any(), any()) } - coVerify(exactly = 1) { arrangement.fileHelper.openAssetFileWithExternalApp(any(), any(), any(), any()) } + coVerify(exactly = 1) { arrangement.fileExternalActions.openLocalFile(any(), any(), any(), any()) } } @Test @@ -247,10 +261,7 @@ class CellViewModelTest { } } - private class Arrangement(conversationId: String? = null) { - - @MockK - lateinit var savedStateHandle: SavedStateHandle + private class Arrangement(private val conversationId: String? = null) { @MockK lateinit var getCellFilesPagedUseCase: GetPaginatedFilesFlowUseCase @@ -267,6 +278,9 @@ class CellViewModelTest { @MockK lateinit var isCellAvailableUseCase: IsAtLeastOneCellAvailableUseCase + @MockK + lateinit var fileExternalActions: CellFileExternalActions + @MockK lateinit var fileHelper: FileHelper @@ -291,14 +305,6 @@ class CellViewModelTest { MockKAnnotations.init(this, relaxUnitFun = true) - mockkObject(ConversationFilesScreenDestination) - every { ConversationFilesScreenDestination.argsFrom(savedStateHandle) } returns CellFilesNavArgs( - conversationId = conversationId - ) - - every { savedStateHandle.get(any()) } returns conversationId - every { savedStateHandle.get("conversationId") } returns conversationId - coEvery { isCellAvailableUseCase.invoke() } returns true.right() coEvery { getCellFilesPagedUseCase.invoke(any(), any(), any(), any()) } returns flowOf( @@ -347,6 +353,22 @@ class CellViewModelTest { coEvery { isCellAvailableUseCase.invoke() } returns false.right() } + fun withShareActionResult(file: CellNodeUi.File) = apply { + every { + cellFileActionsMenu.onMenuItemAction( + conversationId = any(), + parentFolderUuid = any(), + node = file, + action = NodeBottomSheetAction.SHARE, + onResult = any(), + ) + } answers { + @Suppress("UNCHECKED_CAST") + val onResult = invocation.args[4] as (CellFileActionsMenu.MenuActionResult) -> Unit + onResult(CellFileActionsMenu.Share(file)) + } + } + fun arrange(): Pair { every { fileHelper.getCacheDir() } returns File("") @@ -362,12 +384,13 @@ class CellViewModelTest { ) return this to CellViewModel( - savedStateHandle = savedStateHandle, + navArgs = CellFilesNavArgs(conversationId = conversationId), + searchNavArgs = null, getCellFilesPaged = getCellFilesPagedUseCase, deleteCellAsset = deleteCellAssetUseCase, restoreNodeFromRecycleBinUseCase = restoreNodeFromRecycleBinUseCase, isCellAvailable = isCellAvailableUseCase, - fileHelper = fileHelper, + fileExternalActions = fileExternalActions, onlineEditor = onlineEditor, getEditorUrl = getEditorUrlUseCase, cellFileActionsMenu = cellFileActionsMenu, diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/file/CreateFileViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/file/CreateFileViewModelTest.kt index 6fde9f7bd20..e25b916612f 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/file/CreateFileViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/file/CreateFileViewModelTest.kt @@ -17,7 +17,6 @@ */ package com.wire.android.feature.cells.ui.create.file -import androidx.lifecycle.SavedStateHandle import com.wire.android.feature.cells.ui.common.FileNameError import com.wire.kalium.cells.domain.usecase.create.CreateDocumentFileUseCase import com.wire.kalium.cells.domain.usecase.create.CreatePresentationFileUseCase @@ -27,7 +26,6 @@ import com.wire.kalium.common.functional.Either import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first @@ -119,7 +117,7 @@ class CreateFileViewModelTest { advanceUntilIdle() // Then - coVerify(exactly = 1) { arrangement.createDocumentFileUseCase(any()) } + coVerify(exactly = 1) { arrangement.createDocumentFileUseCase("test-uuid/NewDoc") } assertEquals(CreateFileViewModelAction.Success, viewModel.actions.first()) } @@ -137,7 +135,7 @@ class CreateFileViewModelTest { advanceUntilIdle() // Then - coVerify(exactly = 1) { arrangement.createDocumentFileUseCase(any()) } + coVerify(exactly = 1) { arrangement.createDocumentFileUseCase("test-uuid/NewDoc") } assertEquals(CreateFileViewModelAction.Failure, viewModel.actions.first()) } @@ -155,7 +153,7 @@ class CreateFileViewModelTest { advanceUntilIdle() // Then - coVerify(exactly = 1) { arrangement.createSpreadsheetFileUseCase(any()) } + coVerify(exactly = 1) { arrangement.createSpreadsheetFileUseCase("test-uuid/NewSheet") } assertEquals(CreateFileViewModelAction.Success, viewModel.actions.first()) } @@ -173,7 +171,7 @@ class CreateFileViewModelTest { advanceUntilIdle() // Then - coVerify(exactly = 1) { arrangement.createSpreadsheetFileUseCase(any()) } + coVerify(exactly = 1) { arrangement.createSpreadsheetFileUseCase("test-uuid/NewSheet") } assertEquals(CreateFileViewModelAction.Failure, viewModel.actions.first()) } @@ -191,7 +189,7 @@ class CreateFileViewModelTest { advanceUntilIdle() // Then - coVerify(exactly = 1) { arrangement.createPresentationFileUseCase(any()) } + coVerify(exactly = 1) { arrangement.createPresentationFileUseCase("test-uuid/NewSlides") } assertEquals(CreateFileViewModelAction.Success, viewModel.actions.first()) } @@ -209,15 +207,12 @@ class CreateFileViewModelTest { advanceUntilIdle() // Then - coVerify(exactly = 1) { arrangement.createPresentationFileUseCase(any()) } + coVerify(exactly = 1) { arrangement.createPresentationFileUseCase("test-uuid/NewSlides") } assertEquals(CreateFileViewModelAction.Failure, viewModel.actions.first()) } private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var createPresentationFileUseCase: CreatePresentationFileUseCase @@ -228,15 +223,18 @@ class CreateFileViewModelTest { lateinit var createSpreadsheetFileUseCase: CreateSpreadsheetFileUseCase private val testUuid = "test-uuid" + private var navArgs = CreateFileScreenNavArgs( + uuid = testUuid, + fileType = FileType.DOCUMENT, + ) init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.get("uuid") } returns testUuid } private val viewModel by lazy { CreateFileViewModel( - savedStateHandle = savedStateHandle, + navArgs = navArgs, createPresentationFileUseCase = createPresentationFileUseCase, createDocumentFileUseCase = createDocumentFileUseCase, createSpreadsheetFileUseCase = createSpreadsheetFileUseCase, @@ -244,7 +242,7 @@ class CreateFileViewModelTest { } fun withFileTypeReturning(result: FileType) = apply { - every { savedStateHandle.get("fileType") } returns result + navArgs = navArgs.copy(fileType = result) } fun withCreateDocumentFileUseCaseReturning(result: Either) = apply { diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModelTest.kt index 25509aba7e4..683f9fac74f 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModelTest.kt @@ -17,7 +17,6 @@ */ package com.wire.android.feature.cells.ui.create.folder -import androidx.lifecycle.SavedStateHandle import com.wire.android.feature.cells.ui.common.FileNameError import com.wire.kalium.cells.domain.usecase.create.CreateFolderUseCase import com.wire.kalium.common.error.CoreFailure @@ -25,7 +24,6 @@ import com.wire.kalium.common.functional.Either import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -115,7 +113,7 @@ class CreateFolderViewModelTest { advanceUntilIdle() // Then - coVerify(exactly = 1) { arrangement.createFolderUseCase(any()) } + coVerify(exactly = 1) { arrangement.createFolderUseCase("test-uuid/NewFolder") } assertEquals(CreateFolderViewModelAction.Success, viewModel.actions.first()) } @@ -132,28 +130,25 @@ class CreateFolderViewModelTest { advanceUntilIdle() // Then - coVerify(exactly = 1) { arrangement.createFolderUseCase(any()) } + coVerify(exactly = 1) { arrangement.createFolderUseCase("test-uuid/NewFolder") } assertEquals(CreateFolderViewModelAction.Failure, viewModel.actions.first()) } private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var createFolderUseCase: CreateFolderUseCase private val testUuid = "test-uuid" + private val navArgs = CreateFolderScreenNavArgs(uuid = testUuid) init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.get("uuid") } returns testUuid } private val viewModel by lazy { CreateFolderViewModel( - savedStateHandle = savedStateHandle, + navArgs = navArgs, createFolderUseCase = createFolderUseCase, ) } diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModelTest.kt index 371c911964b..8be7e322fa0 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModelTest.kt @@ -17,7 +17,6 @@ */ package com.wire.android.feature.cells.ui.movetofolder -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.android.feature.cells.ui.model.toUiModel import com.wire.kalium.cells.domain.model.Node @@ -27,7 +26,6 @@ import com.wire.kalium.common.error.CoreFailure import com.wire.kalium.common.functional.Either import io.mockk.MockKAnnotations import io.mockk.coEvery -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.StandardTestDispatcher @@ -187,30 +185,26 @@ class MoveToFolderViewModelTest { private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var getFoldersUseCase: GetFoldersUseCase @MockK lateinit var moveNodeUseCase: MoveNodeUseCase - private val navArgsMap = mutableMapOf( - "currentPath" to CURRENT_PATH, - "nodeToMovePath" to NODE_TO_MOVE_PATH, - "uuid" to UUID, - "breadcrumbs" to BREADCRUMBS, + private val navArgs = MoveToFolderNavArgs( + currentPath = CURRENT_PATH, + nodeToMovePath = NODE_TO_MOVE_PATH, + uuid = UUID, + breadcrumbs = BREADCRUMBS, ) init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.get(any()) } answers { navArgsMap[firstArg()] } } private val viewModel by lazy { MoveToFolderViewModel( - savedStateHandle = savedStateHandle, + navArgs = navArgs, getFoldersUseCase = getFoldersUseCase, moveNodeUseCase = moveNodeUseCase, ) diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModelTest.kt index 76147d7d157..cf323ead17f 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModelTest.kt @@ -17,7 +17,6 @@ */ package com.wire.android.feature.cells.ui.publiclink -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.android.feature.cells.util.FileHelper import com.wire.kalium.cells.domain.model.PublicLink @@ -30,7 +29,6 @@ import com.wire.kalium.common.functional.right import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -199,9 +197,6 @@ class PublicLinkViewModelTest { private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var createPublicLinkUseCase: CreatePublicLinkUseCase @@ -214,24 +209,23 @@ class PublicLinkViewModelTest { @MockK lateinit var fileHelper: FileHelper - private val navArgsMap = mutableMapOf( - "assetId" to "assetId", - "fileName" to "fileName", - "publicLinkId" to "publicLinkId", - "isFolder" to false, + private var navArgs = PublicLinkNavArgs( + assetId = "assetId", + fileName = "fileName", + publicLinkId = "publicLinkId", + isFolder = false, ) init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.get(any()) } answers { navArgsMap[firstArg()] } } fun withPublicLink() = apply { - navArgsMap["publicLinkId"] = "publicLinkId" + navArgs = navArgs.copy(publicLinkId = "publicLinkId") } fun withoutPublicLink() = apply { - navArgsMap["publicLinkId"] = null + navArgs = navArgs.copy(publicLinkId = null) } fun withLoadSuccess() = apply { @@ -260,7 +254,7 @@ class PublicLinkViewModelTest { fun arrange(): Pair { return this to PublicLinkViewModel( - savedStateHandle = savedStateHandle, + navArgs = navArgs, createPublicLink = createPublicLinkUseCase, getPublicLinkUseCase = getPublicLinkUseCase, deletePublicLinkUseCase = deletePublicLinkUseCase, diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModelTest.kt index 8500d91c569..df6e0a62561 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModelTest.kt @@ -17,7 +17,6 @@ */ package com.wire.android.feature.cells.ui.publiclink.settings.expiration -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.android.ui.common.datetime.TimePickerResult import com.wire.kalium.cells.domain.usecase.publiclink.SetPublicLinkExpirationUseCase @@ -297,13 +296,11 @@ class PublicLinkExpirationScreenViewModelTest { fun arrange(): Pair { return this to PublicLinkExpirationScreenViewModel( + navArgs = PublicLinkExpirationScreenNavArgs( + linkUuid = "public_link_uuid", + expiresAt = expiresAt, + ), setExpiration = setExpiration, - savedStateHandle = SavedStateHandle( - mapOf( - "linkUuid" to "public_link_uuid", - "expiresAt" to expiresAt - ) - ) ) } } diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModelTest.kt index a13a849f008..b8876135171 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModelTest.kt @@ -18,7 +18,6 @@ package com.wire.android.feature.cells.ui.publiclink.settings.password import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.kalium.cells.domain.model.PublicLink import com.wire.kalium.cells.domain.usecase.publiclink.CreatePublicLinkPasswordUseCase @@ -371,18 +370,17 @@ class PublicLinkPasswordScreenViewModelTest { private class Arrangement { - private val navArgsMap = mutableMapOf( - "linkUuid" to testLink.uuid, - "passwordEnabled" to false, + private var navArgs = PublicLinkPasswordNavArgs( + linkUuid = testLink.uuid, + passwordEnabled = false, ) init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.get(any()) } answers { navArgsMap[firstArg()] } } fun withPasswordEnabled(enabled: Boolean) = apply { - navArgsMap["passwordEnabled"] = enabled + navArgs = navArgs.copy(passwordEnabled = enabled) } fun withPasswordRemoveSuccess() = apply { @@ -423,19 +421,16 @@ class PublicLinkPasswordScreenViewModelTest { @MockK lateinit var getLocalPassword: GetPublicLinkPasswordUseCase - @MockK - lateinit var savedStateHandle: SavedStateHandle - fun arrange(): Pair { every { generateRandomPassword() } returns randomPassword return this to PublicLinkPasswordScreenViewModel( + navArgs = navArgs, generateRandomPassword = generateRandomPassword, createPassword = createPassword, updatePassword = updatePassword, getPublicLinkPassword = getLocalPassword, - savedStateHandle = savedStateHandle ) } } diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/rename/RenameNodeViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/rename/RenameNodeViewModelTest.kt index 1eb828d117c..da7d3ecdce3 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/rename/RenameNodeViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/rename/RenameNodeViewModelTest.kt @@ -17,9 +17,7 @@ */ package com.wire.android.feature.cells.ui.rename -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.ramcosta.composedestinations.generated.cells.navArgs import com.wire.android.feature.cells.ui.common.FileNameError import com.wire.kalium.cells.domain.usecase.RenameNodeFailure import com.wire.kalium.cells.domain.usecase.RenameNodeUseCase @@ -29,7 +27,6 @@ import com.wire.kalium.common.functional.left import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.StandardTestDispatcher @@ -174,31 +171,23 @@ class RenameNodeViewModelTest { private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var renameNodeUseCase: RenameNodeUseCase - init { + private var navArgs = RenameNodeNavArgs( + uuid = UUID, + currentPath = CURRENT_PATH, + nodeName = NODE_NAME, + isFolder = false + ) + init { MockKAnnotations.init(this, relaxUnitFun = true) - - every { savedStateHandle.navArgs() } returns RenameNodeNavArgs( - uuid = UUID, - currentPath = CURRENT_PATH, - nodeName = NODE_NAME, - isFolder = true - ) - every { savedStateHandle.get("uuid") } returns UUID - every { savedStateHandle.get("currentPath") } returns CURRENT_PATH - every { savedStateHandle.get("isFolder") } returns false - every { savedStateHandle.get("nodeName") } returns NODE_NAME } private val viewModel by lazy { RenameNodeViewModel( - savedStateHandle = savedStateHandle, + navArgs = navArgs, renameNodeUseCase = renameNodeUseCase, ) } @@ -207,7 +196,7 @@ class RenameNodeViewModelTest { coEvery { renameNodeUseCase(any(), any(), any()) } returns result } fun withNodeNameReturning(name: String) = apply { - every { savedStateHandle.get("nodeName") } returns name + navArgs = navArgs.copy(nodeName = name) } fun arrange() = this to viewModel diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/search/SearchScreenViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/search/SearchScreenViewModelTest.kt index 7cc08ffd1d8..4017d248cd6 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/search/SearchScreenViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/search/SearchScreenViewModelTest.kt @@ -17,7 +17,6 @@ */ package com.wire.android.feature.cells.ui.search -import androidx.lifecycle.SavedStateHandle import androidx.paging.PagingData import com.wire.android.feature.cells.ui.CellFileLocalPathCache import com.wire.android.feature.cells.ui.search.filter.data.FilterConversationUi @@ -37,7 +36,6 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow @@ -79,24 +77,10 @@ class SearchScreenViewModelTest { private val sharedPathCache = CellFileLocalPathCache() - private lateinit var savedStateHandle: SavedStateHandle - @BeforeEach fun setup() { Dispatchers.setMain(dispatcher) - val navArgsMap = mapOf( - "conversationId" to CONVERSATION_ID, - "screenType" to DriveSearchScreenType.SHARED_DRIVE - ) - - savedStateHandle = mockk(relaxed = true) - - every { savedStateHandle.get(any()) } answers { - val key = firstArg() - navArgsMap[key] - } - MockKAnnotations.init(this) coEvery { getAllTagsUseCase() } returns Either.Right(mockTags) @@ -149,18 +133,13 @@ class SearchScreenViewModelTest { @Test fun `given parentRoute in nav args, when ViewModel is created, then parentRoute is exposed`() = runTest { val expectedRoute = "app/global_cells_screen" - val navArgsWithParentRoute = mapOf( - "conversationId" to CONVERSATION_ID, - "screenType" to DriveSearchScreenType.SHARED_DRIVE, - "parentRoute" to expectedRoute, - ) - val savedStateHandleWithParentRoute = mockk(relaxed = true) - every { savedStateHandleWithParentRoute.get(any()) } answers { - navArgsWithParentRoute[firstArg()] - } val viewModel = SearchScreenViewModel( - savedStateHandle = savedStateHandleWithParentRoute, + navArgs = SearchNavArgs( + conversationId = CONVERSATION_ID, + screenType = DriveSearchScreenType.SHARED_DRIVE, + parentRoute = expectedRoute, + ), getAllTagsUseCase = getAllTagsUseCase, getCellFilesPaged = getCellFilesPaged, getOwners = getOwners, @@ -359,7 +338,10 @@ class SearchScreenViewModelTest { private fun createViewModel(): SearchScreenViewModel { return SearchScreenViewModel( - savedStateHandle = savedStateHandle, + navArgs = SearchNavArgs( + conversationId = CONVERSATION_ID, + screenType = DriveSearchScreenType.SHARED_DRIVE, + ), getAllTagsUseCase = getAllTagsUseCase, getCellFilesPaged = getCellFilesPaged, getOwners = getOwners, @@ -369,14 +351,11 @@ class SearchScreenViewModelTest { } private fun createViewModelWithScreenType(screenType: DriveSearchScreenType): SearchScreenViewModel { - val navArgsMap = mapOf( - "conversationId" to CONVERSATION_ID, - "screenType" to screenType, - ) - val handle = mockk(relaxed = true) - every { handle.get(any()) } answers { navArgsMap[firstArg()] } return SearchScreenViewModel( - savedStateHandle = handle, + navArgs = SearchNavArgs( + conversationId = CONVERSATION_ID, + screenType = screenType, + ), getAllTagsUseCase = getAllTagsUseCase, getCellFilesPaged = getCellFilesPaged, getOwners = getOwners, diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModelTest.kt index ff78972d44a..f63600f7364 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModelTest.kt @@ -18,7 +18,6 @@ package com.wire.android.feature.cells.ui.tags import androidx.compose.foundation.text.input.setTextAndSelectAll -import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.wire.android.feature.cells.ui.movetofolder.MoveToFolderViewModelTest.Companion.UUID import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase @@ -29,7 +28,6 @@ import com.wire.kalium.common.functional.Either import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.StandardTestDispatcher @@ -293,9 +291,6 @@ class AddRemoveTagsViewModelTest { private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var getAllTagsUseCase: GetAllTagsUseCase @@ -305,10 +300,14 @@ class AddRemoveTagsViewModelTest { @MockK lateinit var removeNodeTagsUseCase: RemoveNodeTagsUseCase + private val navArgs = AddRemoveTagsNavArgs( + uuid = UUID, + tags = ArrayList() + ) + init { MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.get("uuid") } returns UUID - every { savedStateHandle.get>("tags") } returns ArrayList() + coEvery { getAllTagsUseCase() } returns Either.Right(emptySet()) } fun withGetAllTagsUseCaseReturning(result: Either>) = apply { @@ -327,7 +326,7 @@ class AddRemoveTagsViewModelTest { // Create a new ViewModel instance every time arrange() is called. // This prevents state from leaking between tests. val viewModel = AddRemoveTagsViewModel( - savedStateHandle = savedStateHandle, + navArgs = navArgs, getAllTagsUseCase = getAllTagsUseCase, updateNodeTagsUseCase = updateNodeTagsUseCase, removeNodeTagsUseCase = removeNodeTagsUseCase, diff --git a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelTest.kt b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelTest.kt index a97cb3d3f58..81ebb603891 100644 --- a/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelTest.kt +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelTest.kt @@ -17,7 +17,6 @@ */ package com.wire.android.feature.cells.ui.versioning -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.TestDispatcherProvider import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.edit.OnlineEditor @@ -82,7 +81,6 @@ class VersionHistoryViewModelTest { @Test fun givenViewModel_whenItInits_thenIsFetchingStateIsManagedCorrectly() = runTest { val (_, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(emptyList())) .arrange() @@ -99,7 +97,6 @@ class VersionHistoryViewModelTest { val twoDaysAgo = today.minusDays(2) val (_, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(versionsFromApi)) .withFileSizeFormatter() .arrange() @@ -145,7 +142,6 @@ class VersionHistoryViewModelTest { @Test fun givenApiFailure_whenViewModelInits_thenVersionListIsEmpty() = runTest { val (_, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Left(CoreFailure.MissingClientRegistration)) .arrange() @@ -160,7 +156,6 @@ class VersionHistoryViewModelTest { // GIVEN an initial state where the dialog is not visible val testVersionId = "version-id-12345" val (_, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(emptyList())) .arrange() @@ -182,7 +177,6 @@ class VersionHistoryViewModelTest { fun givenDialogIsVisible_whenHideRestoreConfirmationDialogIsCalled_thenStateIsHiddenAndReset() = runTest { // GIVEN an initial state where the dialog is visible and has data val (_, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(emptyList())) .arrange() @@ -210,7 +204,6 @@ class VersionHistoryViewModelTest { // GIVEN the restore use case will succeed val testVersionId = "version-to-restore" val (arrangement, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(emptyList())) .withRestoreNodeVersionReturning(Unit.right()) .arrange() @@ -238,7 +231,6 @@ class VersionHistoryViewModelTest { // GIVEN val testVersionId = "version-to-restore" val (arrangement, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(emptyList())) .withRestoreNodeVersionReturning(Either.Left(CoreFailure.MissingClientRegistration)) .arrange() @@ -263,7 +255,6 @@ class VersionHistoryViewModelTest { fun givenVersionExistsAndUseCaseSucceeds_whenDownloadVersionIsCalled_thenStateBecomesDownloaded() = runTest { // GIVEN a version exists and all dependencies will succeed val (_, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withDownloadVersionReturning(shouldSucceed = true, true) .withGetNodeVersionReturning(Either.Right(versionsFromApi)) .withFileSizeFormatter() @@ -287,7 +278,6 @@ class VersionHistoryViewModelTest { fun givenDownloadUseCaseFails_whenDownloadVersionIsCalled_thenStateBecomesFailed() = runTest { // GIVEN a version exists but the download use case will fail val (_, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(emptyList())) .withDownloadVersionReturning(shouldSucceed = false) .withFileSizeFormatter() @@ -312,7 +302,6 @@ class VersionHistoryViewModelTest { fun givenFileCreationFails_whenDownloadVersionIsCalled_thenStateBecomesFailed() = runTest { // GIVEN a version exists but the file helper returns null (cannot create file) val (arrangement, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(emptyList())) .withFileSizeFormatter() .withFileCreationFailure() @@ -336,7 +325,6 @@ class VersionHistoryViewModelTest { @Test fun givenVersionDoesNotExist_whenDownloadVersionIsCalled_thenStateBecomesFailed() = runTest { val (arrangement, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(emptyList())) .withFileSizeFormatter() .withFileCreationFailure() @@ -357,7 +345,6 @@ class VersionHistoryViewModelTest { // Given val expectedUrl = "https://example.com/editor" val (arrangement, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(emptyList())) .withGetEditorUrlReturning(expectedUrl.right()) .withOnlineEditor() @@ -376,7 +363,6 @@ class VersionHistoryViewModelTest { fun givenGetEditorUrlFails_whenOpenOnlineEditorIsCalled_thenEditorIsNotOpened() = runTest { // Given val (arrangement, viewModel) = Arrangement() - .withSavedStateHandleReturning() .withGetNodeVersionReturning(Either.Right(emptyList())) .withGetEditorUrlReturning(Either.Left(CoreFailure.MissingClientRegistration)) .withOnlineEditor() @@ -393,7 +379,6 @@ class VersionHistoryViewModelTest { private class Arrangement { - val savedStateHandle: SavedStateHandle = mockk(relaxed = true) val getNodeVersionsUseCase: GetNodeVersionsUseCase = mockk() val fileSizeFormatter: FileSizeFormatter = mockk() val restoreNodeVersionUseCase: RestoreNodeVersionUseCase = mockk() @@ -405,16 +390,6 @@ class VersionHistoryViewModelTest { private val testNodeUuid = "test-node-uuid" - init { - every { savedStateHandle.get("uuid") } returns "test-node-uuid" - every { savedStateHandle.get("fileName") } returns "file-name" - } - - fun withSavedStateHandleReturning() = apply { - every { savedStateHandle.get("uuid") } returns testNodeUuid - every { savedStateHandle.get("fileName") } returns "file-name" - } - fun withGetNodeVersionReturning(returnValue: Either>) = apply { coEvery { getNodeVersionsUseCase(testNodeUuid) } returns returnValue } @@ -469,7 +444,7 @@ class VersionHistoryViewModelTest { fun arrange(): Pair { val viewModel = VersionHistoryViewModel( - savedStateHandle = savedStateHandle, + navArgs = VersionHistoryNavArgs(uuid = testNodeUuid, fileName = "file-name"), getNodeVersionsUseCase = getNodeVersionsUseCase, fileSizeFormatter = fileSizeFormatter, restoreNodeVersionUseCase = restoreNodeVersionUseCase, diff --git a/features/meetings/build.gradle.kts b/features/meetings/build.gradle.kts index b6f34267b1b..254e0ef5603 100644 --- a/features/meetings/build.gradle.kts +++ b/features/meetings/build.gradle.kts @@ -1,7 +1,6 @@ plugins { id(libs.plugins.wire.android.library.get().pluginId) id(libs.plugins.wire.kover.get().pluginId) - id(libs.plugins.wire.hilt.get().pluginId) id(BuildPlugins.kotlinParcelize) id(BuildPlugins.junit5) alias(libs.plugins.ksp) @@ -13,20 +12,17 @@ plugins { dependencies { implementation("com.wire.kalium:kalium-common") implementation("com.wire.kalium:kalium-logic") + implementation(project(":core:di")) implementation(project(":core:ui-common")) implementation(libs.androidx.core) implementation(libs.androidx.appcompat) implementation(libs.ktx.immutableCollections) implementation(libs.ktx.serialization) - // hilt - implementation(libs.hilt.navigationCompose) - implementation(libs.hilt.work) - // smaller view models implementation(libs.resaca.core) - implementation(libs.resaca.hilt) implementation(libs.bundlizer.core) + implementation(libs.dagger) val composeBom = platform(libs.compose.bom) implementation(composeBom) diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/MeetingViewModelGraph.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/MeetingViewModelGraph.kt new file mode 100644 index 00000000000..c2b2dd3cef5 --- /dev/null +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/MeetingViewModelGraph.kt @@ -0,0 +1,27 @@ +/* + * 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.feature.meetings.ui + +import com.wire.android.di.metro.MetroViewModelGraph +import com.wire.android.feature.meetings.ui.list.MeetingListViewModelFactory +import com.wire.android.feature.meetings.ui.options.MeetingOptionsMenuViewModelFactory + +interface MeetingViewModelGraph : MetroViewModelGraph { + val meetingListViewModelFactory: MeetingListViewModelFactory + val meetingOptionsMenuViewModelFactory: MeetingOptionsMenuViewModelFactory +} diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingList.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingList.kt index 337e0f7d4d4..55f9931b1fa 100644 --- a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingList.kt +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingList.kt @@ -26,16 +26,17 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.meetings.R import com.wire.android.feature.meetings.model.MeetingHeader import com.wire.android.feature.meetings.model.MeetingItem import com.wire.android.feature.meetings.model.MeetingListItem +import com.wire.android.feature.meetings.ui.MeetingViewModelGraph import com.wire.android.feature.meetings.ui.MeetingsTabItem import com.wire.android.feature.meetings.ui.util.CurrentTimeProvider import com.wire.android.feature.meetings.ui.util.PreviewMultipleThemes @@ -52,12 +53,9 @@ fun MeetingList( openMeetingOptions: (meetingId: String) -> Unit = {}, meetingListViewModel: MeetingListViewModel = when { LocalInspectionMode.current -> MeetingListViewModelPreview(CurrentTimeProvider.Preview, type) - else -> hiltViewModel( - key = "meeting_list_${type.name}", - creationCallback = { factory -> - factory.create(type = type) - } - ) + else -> metroViewModel(key = "meeting_list_${type.name}") { + meetingListViewModelFactory.create(type = type) + } }, ) { val lazyPagingItems = meetingListViewModel.meetings.collectAsLazyPagingItems() diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingListViewModel.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingListViewModel.kt index cacc49ab47d..0bb44dea5fa 100644 --- a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingListViewModel.kt +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingListViewModel.kt @@ -31,10 +31,6 @@ import com.wire.android.feature.meetings.ui.MeetingsTabItem import com.wire.android.feature.meetings.ui.mock.MeetingMocksProvider import com.wire.android.feature.meetings.ui.util.CurrentTimeProvider import com.wire.android.util.dispatchers.DispatcherProvider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -66,16 +62,10 @@ class MeetingListViewModelPreview( MutableStateFlow(PagingData.from(meetingMocksProvider.getItems(showingAll, type).insertHeaders(type))) } -@HiltViewModel(assistedFactory = MeetingListViewModelImpl.Factory::class) -class MeetingListViewModelImpl @AssistedInject constructor( - @Assisted val type: MeetingsTabItem, +class MeetingListViewModelImpl( + val type: MeetingsTabItem, dispatcher: DispatcherProvider, ) : ViewModel(), MeetingListViewModel { - @AssistedFactory - interface Factory { - fun create(type: MeetingsTabItem): MeetingListViewModelImpl - } - override val isShowingAll = MutableStateFlow(type == MeetingsTabItem.PAST) // for PAST always show all, for NEXT start with false private val meetingMocksProvider = MeetingMocksProvider(CurrentTimeProvider.Default) // TODO replace with real data source override val meetings: Flow> = isShowingAll diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingListViewModelFactory.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingListViewModelFactory.kt new file mode 100644 index 00000000000..b4382633d63 --- /dev/null +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingListViewModelFactory.kt @@ -0,0 +1,32 @@ +/* + * 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.feature.meetings.ui.list + +import com.wire.android.feature.meetings.ui.MeetingsTabItem +import com.wire.android.util.dispatchers.DispatcherProvider +import dev.zacsweers.metro.Inject + +@Inject +class MeetingListViewModelFactory( + private val dispatcher: DispatcherProvider, +) { + fun create(type: MeetingsTabItem): MeetingListViewModelImpl = MeetingListViewModelImpl( + type = type, + dispatcher = dispatcher, + ) +} diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsMenuViewModel.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsMenuViewModel.kt index 70877efa12f..c2aef80d5fd 100644 --- a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsMenuViewModel.kt +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsMenuViewModel.kt @@ -22,10 +22,8 @@ import com.wire.android.feature.meetings.model.MeetingItem import com.wire.android.feature.meetings.ui.mock.MeetingMocksProvider import com.wire.android.feature.meetings.ui.mock.scheduledRepeatingGroupMeeting import com.wire.android.feature.meetings.ui.util.CurrentTimeProvider -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject interface MeetingOptionsMenuViewModel { fun observeMeetingStateFlow(meetingId: String): StateFlow = MutableStateFlow( @@ -42,8 +40,7 @@ class MeetingOptionsMenuViewModelPreview(currentTimeProvider: CurrentTimeProvide ) } -@HiltViewModel -class MeetingOptionsMenuViewModelImpl @Inject constructor() : MeetingOptionsMenuViewModel, ViewModel() { +class MeetingOptionsMenuViewModelImpl : MeetingOptionsMenuViewModel, ViewModel() { private val meetingMocksProvider = MeetingMocksProvider(CurrentTimeProvider.Default) // TODO replace with real data source override fun observeMeetingStateFlow(meetingId: String): StateFlow = MutableStateFlow( meetingMocksProvider.getItem(meetingId)?.let { diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsMenuViewModelFactory.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsMenuViewModelFactory.kt new file mode 100644 index 00000000000..c4b767d7894 --- /dev/null +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsMenuViewModelFactory.kt @@ -0,0 +1,25 @@ +/* + * 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.feature.meetings.ui.options + +import dev.zacsweers.metro.Inject + +@Inject +class MeetingOptionsMenuViewModelFactory { + fun create(): MeetingOptionsMenuViewModelImpl = MeetingOptionsMenuViewModelImpl() +} diff --git a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsModalSheetLayout.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsModalSheetLayout.kt index f9e2946696a..2c0b6644236 100644 --- a/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsModalSheetLayout.kt +++ b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsModalSheetLayout.kt @@ -25,10 +25,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.meetings.R import com.wire.android.feature.meetings.model.MeetingItem +import com.wire.android.feature.meetings.ui.MeetingViewModelGraph import com.wire.android.feature.meetings.ui.list.MeetingLeadingIcon import com.wire.android.feature.meetings.ui.list.VideoCallIcon import com.wire.android.feature.meetings.ui.mock.scheduledRepeatingGroupMeeting @@ -53,7 +54,9 @@ fun MeetingOptionsModalSheetLayout( sheetState: WireModalSheetState, viewModel: MeetingOptionsMenuViewModel = when { LocalInspectionMode.current -> MeetingOptionsMenuViewModelPreview(CurrentTimeProvider.Preview) - else -> hiltViewModel() + else -> metroViewModel { + meetingOptionsMenuViewModelFactory.create() + } } ) { val deletedMeetingOptionsClosedMessage = stringResource(R.string.deleted_meeting_options_closed) diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasScreen.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasScreen.kt index 0fbf7d5d781..d1426265195 100644 --- a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasScreen.kt +++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasScreen.kt @@ -82,6 +82,9 @@ fun DrawingCanvasScreen( ) { val context = LocalContext.current val scope = rememberCoroutineScope() + val sketchImageSaver = remember(context) { + AndroidSketchImageSaver(context.applicationContext) + } val discardDrawing: () -> Unit = remember { { viewModel.initializeCanvas() @@ -110,10 +113,15 @@ fun DrawingCanvasScreen( onStopDrawing = viewModel::onStopDrawing, onDismissEvent = onDismissEvent, onUndoStroke = viewModel::onUndoLastStroke, - onSendSketch = remember { + onSendSketch = remember(scope, sketchImageSaver, viewModel, resultNavigator, drawingCanvasNavArgs.tempWritableUri) { { scope.launch { - resultNavigator.setResult(DrawingCanvasNavBackArgs(viewModel.saveImage(context))) + val savedImageUri = sketchImageSaver.save( + state = viewModel.state, + tempWritableUri = drawingCanvasNavArgs.tempWritableUri + ) + viewModel.initializeCanvas() + resultNavigator.setResult(DrawingCanvasNavBackArgs(savedImageUri)) resultNavigator.navigateBack() } } diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt index 693b637aa72..1e4387d601e 100644 --- a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt +++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt @@ -17,40 +17,21 @@ */ package com.wire.android.feature.sketch -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint -import android.net.Uri -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.core.net.toUri -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.ramcosta.composedestinations.generated.sketch.destinations.DrawingCanvasScreenDestination -import com.wire.android.feature.sketch.model.DrawingCanvasNavArgs import com.wire.android.feature.sketch.model.DrawingMotionEvent import com.wire.android.feature.sketch.model.DrawingPathProperties import com.wire.android.feature.sketch.model.DrawingState import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream @Suppress("TooManyFunctions") -class DrawingCanvasViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { - - private val drawingCanvasNavArgs: DrawingCanvasNavArgs = DrawingCanvasScreenDestination.argsFrom(savedStateHandle) +class DrawingCanvasViewModel : ViewModel() { internal var state: DrawingState by mutableStateOf(DrawingState()) private set @@ -144,42 +125,6 @@ class DrawingCanvasViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { } } - /** - * Saves the image to the provided URI and resets the canvas. - * - * @param context The context to use to open the file descriptor. - * - * @return The [Uri] of the saved image. - */ - suspend fun saveImage(context: Context): Uri { - val tempSketchFile = drawingCanvasNavArgs.tempWritableUri.orTempUri(context) - viewModelScope.launch { - withContext(Dispatchers.IO) { - with(state) { - if (canvasSize == null || state.paths.isEmpty()) return@withContext - - val bitmap = Bitmap.createBitmap( - canvasSize.width.toInt(), - canvasSize.height.toInt(), - Bitmap.Config.ARGB_8888 - ) - val canvas = Canvas(bitmap).apply { drawPaint(Paint().apply { color = Color.White.toArgb() }) } - context.contentResolver.openFileDescriptor(tempSketchFile, "rwt")?.use { fileDescriptor -> - FileOutputStream(fileDescriptor.fileDescriptor).use { fileOutputStream -> - paths.forEach { path -> path.drawNative(canvas) } - bitmap.compress(Bitmap.CompressFormat.JPEG, QUALITY, fileOutputStream) - fileOutputStream.flush() - }.also { - Log.d("DrawingCanvasViewModel", "Image written to: $tempSketchFile") - } - } - } - } - }.join() - initializeCanvas() - return tempSketchFile - } - fun onColorChanged(selectedColor: Color) { state = state.copy( currentPath = DrawingPathProperties().apply { @@ -189,14 +134,4 @@ class DrawingCanvasViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { } ) } - - private fun Uri?.orTempUri(context: Context): Uri = this ?: run { - val tempFile = File.createTempFile("temp_sketch", ".jpg", context.cacheDir) - tempFile.deleteOnExit() - tempFile.toUri() - } - - companion object { - private const val QUALITY = 50 - } } diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/SketchImageSaver.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/SketchImageSaver.kt new file mode 100644 index 00000000000..0ddc3d38e63 --- /dev/null +++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/SketchImageSaver.kt @@ -0,0 +1,77 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.sketch + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.net.Uri +import android.util.Log +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.core.net.toUri +import com.wire.android.feature.sketch.model.DrawingState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream + +internal interface SketchImageSaver { + suspend fun save(state: DrawingState, tempWritableUri: Uri?): Uri +} + +internal class AndroidSketchImageSaver(private val context: Context) : SketchImageSaver { + + override suspend fun save(state: DrawingState, tempWritableUri: Uri?): Uri = withContext(Dispatchers.IO) { + val tempSketchFile = tempWritableUri.orTempUri(context) + with(state) { + if (canvasSize == null || paths.isEmpty()) return@withContext tempSketchFile + + val bitmap = Bitmap.createBitmap( + canvasSize.width.toInt(), + canvasSize.height.toInt(), + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap).apply { + drawPaint(Paint().apply { color = Color.White.toArgb() }) + } + context.contentResolver.openFileDescriptor(tempSketchFile, "rwt")?.use { fileDescriptor -> + FileOutputStream(fileDescriptor.fileDescriptor).use { fileOutputStream -> + paths.forEach { path -> path.drawNative(canvas) } + bitmap.compress(Bitmap.CompressFormat.JPEG, QUALITY, fileOutputStream) + fileOutputStream.flush() + }.also { + Log.d(TAG, "Image written to: $tempSketchFile") + } + } + } + tempSketchFile + } + + private fun Uri?.orTempUri(context: Context): Uri = this ?: run { + val tempFile = File.createTempFile("temp_sketch", ".jpg", context.cacheDir) + tempFile.deleteOnExit() + tempFile.toUri() + } + + private companion object { + const val QUALITY = 50 + const val TAG = "AndroidSketchImageSaver" + } +} diff --git a/features/sketch/src/test/java/com/wire/android/feature/sketch/DrawingCanvasViewModelTest.kt b/features/sketch/src/test/java/com/wire/android/feature/sketch/DrawingCanvasViewModelTest.kt index 13ce7227f1f..87076f1f646 100644 --- a/features/sketch/src/test/java/com/wire/android/feature/sketch/DrawingCanvasViewModelTest.kt +++ b/features/sketch/src/test/java/com/wire/android/feature/sketch/DrawingCanvasViewModelTest.kt @@ -1,21 +1,12 @@ package com.wire.android.feature.sketch -import android.net.Uri import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.lifecycle.SavedStateHandle -import com.ramcosta.composedestinations.generated.sketch.destinations.DrawingCanvasScreenDestination -import com.wire.android.feature.sketch.model.DrawingCanvasNavArgs import com.wire.android.feature.sketch.model.DrawingMotionEvent -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -@ExtendWith(NavigationTestExtension::class) class DrawingCanvasViewModelTest { @Test @@ -191,22 +182,10 @@ class DrawingCanvasViewModelTest { private class Arrangement { - @MockK - lateinit var savedStateHandle: SavedStateHandle - - @MockK - lateinit var tempWritableUri: Uri - private val viewModel by lazy { - DrawingCanvasViewModel(savedStateHandle) + DrawingCanvasViewModel() } - init { - MockKAnnotations.init(this, relaxUnitFun = true) - every { - DrawingCanvasScreenDestination.argsFrom(savedStateHandle) - } returns DrawingCanvasNavArgs("Conversation Name", tempWritableUri) - } fun arrange() = this to viewModel } diff --git a/features/sync/build.gradle.kts b/features/sync/build.gradle.kts index 5ab29427f29..aa11036df82 100644 --- a/features/sync/build.gradle.kts +++ b/features/sync/build.gradle.kts @@ -1,7 +1,6 @@ plugins { id(libs.plugins.wire.android.library.get().pluginId) id(libs.plugins.wire.kover.get().pluginId) - id(libs.plugins.wire.hilt.get().pluginId) id(BuildPlugins.kotlinParcelize) id(BuildPlugins.junit5) alias(libs.plugins.ksp) @@ -19,15 +18,12 @@ dependencies { implementation(libs.androidx.core) implementation(libs.androidx.appcompat) - // hilt - implementation(libs.hilt.navigationCompose) - implementation(libs.hilt.work) implementation(libs.androidx.work) // smaller view models implementation(libs.resaca.core) - implementation(libs.resaca.hilt) implementation(libs.bundlizer.core) + implementation(libs.dagger) val composeBom = platform(libs.compose.bom) implementation(composeBom) diff --git a/features/sync/src/main/kotlin/com/wire/android/sync/InitialSyncWorker.kt b/features/sync/src/main/kotlin/com/wire/android/sync/InitialSyncWorker.kt index f4590eb67f9..c07be326b25 100644 --- a/features/sync/src/main/kotlin/com/wire/android/sync/InitialSyncWorker.kt +++ b/features/sync/src/main/kotlin/com/wire/android/sync/InitialSyncWorker.kt @@ -20,7 +20,6 @@ package com.wire.android.sync import android.content.Context import android.util.Log import androidx.core.app.NotificationCompat -import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.Data import androidx.work.ForegroundInfo @@ -34,18 +33,15 @@ import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.feature.session.GetAllSessionsResult import com.wire.kalium.work.Work import com.wire.kalium.work.WorkId -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.launch import com.wire.android.feature.notification.R as NR -@HiltWorker -class InitialSyncWorker @AssistedInject constructor( - @Assisted context: Context, - @Assisted parameters: WorkerParameters, +class InitialSyncWorker( + context: Context, + parameters: WorkerParameters, @KaliumCoreLogic private val coreLogic: CoreLogic, private val notificationChannelsManager: NotificationChannelsManager, ) : CoroutineWorker(context, parameters) { diff --git a/features/sync/src/main/kotlin/com/wire/android/sync/MonitorSyncWorkUseCase.kt b/features/sync/src/main/kotlin/com/wire/android/sync/MonitorSyncWorkUseCase.kt index 1af66e0a8c3..7a94ece65a7 100644 --- a/features/sync/src/main/kotlin/com/wire/android/sync/MonitorSyncWorkUseCase.kt +++ b/features/sync/src/main/kotlin/com/wire/android/sync/MonitorSyncWorkUseCase.kt @@ -28,7 +28,7 @@ import com.wire.kalium.logic.feature.session.GetAllSessionsResult import com.wire.kalium.logic.feature.session.ObserveSessionsUseCase import com.wire.kalium.work.Work import com.wire.kalium.work.WorkId -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d93d255193b..41daec220ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,8 @@ desugaring = "2.1.5" firebaseBOM = "34.7.0" fragment = "1.5.6" resaca = "5.0.2" +resacaMetro = "5.0.0" +metro = "1.1.1" bundlizer = "0.8.0" squareup-javapoet = "1.13.0" visibilityModifiers = "1.1.0" @@ -133,6 +135,7 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutLibraries" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +metro = { id = "dev.zacsweers.metro", version.ref = "metro" } screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"} compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } cyclonedx = { id = "org.cyclonedx.bom", version.ref = "cyclonedx" } @@ -159,10 +162,12 @@ kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-pl android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "android-gradlePlugin" } android-gradleApi = { group = "com.android.tools.build", name = "gradle-api", version.ref = "android-gradlePlugin" } hilt-gradlePlugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" } +dagger = { module = "com.google.dagger:dagger", version.ref = "hilt" } googleGms-gradlePlugin = { module = "com.google.gms:google-services", version.ref = "google-gms" } googleGms-location = { module = "com.google.android.gms:play-services-location", version.ref = "gms-location" } aboutLibraries-gradlePlugin = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutLibraries" } kover-gradlePlugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } +metro-gradlePlugin = { module = "dev.zacsweers.metro:dev.zacsweers.metro.gradle.plugin", version.ref = "metro" } ktx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "ktx-serialization" } ktx-dateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "ktx-dateTime" } ktx-immutableCollections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "ktx-immutableCollections" } @@ -181,6 +186,7 @@ allure-kotlin-android = { module = "io.qameta.allure:allure-kotlin-android", ver # android dependencies # KotlinX +coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } @@ -190,6 +196,7 @@ squareup-javapoet = { module = "com.squareup:javapoet", version.ref = "squareup- visibilityModifiers = { module = "io.github.esentsov:kotlin-visibility", version.ref = "visibilityModifiers" } resaca-core = { module = "io.github.sebaslogen:resaca", version.ref = "resaca" } resaca-hilt = { module = "io.github.sebaslogen:resacahilt", version.ref = "resaca" } +resaca-metro = { module = "io.github.sebaslogen:resacametro", version.ref = "resacaMetro" } bundlizer-core = { module = "dev.ahmedmourad.bundlizer:bundlizer-core", version.ref = "bundlizer" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBOM" } firebase-fcm = { module = "com.google.firebase:firebase-messaging" } @@ -224,7 +231,6 @@ enterprise-feedback = { group = "androidx.enterprise", name = "enterprise-feedba hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } hilt-navigationCompose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-composeNavigation" } -hilt-test = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hilt-work" } # Compose BOM diff --git a/kalium b/kalium index cc41a919dac..6cc424ce331 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit cc41a919dacdfbc4c82ceff0493a1e09f0d7a46a +Subproject commit 6cc424ce3317f7b4b5ddc42a234aef82e1cac68a diff --git a/settings.gradle.kts b/settings.gradle.kts index 24aa54deccc..4a6aea46fb0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,7 +27,7 @@ pluginManagement { } // Include all the existent modules in the project -val basePathModules = setOf("features", "core", "tests") +val basePathModules = setOf("features", "core", "tests", "shared") val ignorableModules = setOf("buildSrc", "kalium") rootDir .walk() diff --git a/shared/auth/build.gradle.kts b/shared/auth/build.gradle.kts new file mode 100644 index 00000000000..e598309bcf6 --- /dev/null +++ b/shared/auth/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id(libs.plugins.wire.kmp.library.get().pluginId) + alias(libs.plugins.metro) +} + +kotlin { + android { + namespace = "com.wire.shared.auth" + } + + sourceSets { + val commonMain by getting { + dependencies { + api(libs.coroutines.core) + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(libs.coroutines.test) + } + } + } +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/AuthLoginSuccessPayload.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/AuthLoginSuccessPayload.kt new file mode 100644 index 00000000000..79c13755c40 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/AuthLoginSuccessPayload.kt @@ -0,0 +1,43 @@ +/* + * 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.shared.auth + +data class AuthLoginSuccessPayload( + val userIdValue: String, + val userIdDomain: String?, + val accessTokenValue: String, + val accessTokenType: String, + val accessTokenExpiresInSeconds: Int?, + val refreshTokenValue: String, + val refreshTokenCookieName: String = REFRESH_TOKEN_COOKIE_NAME, + val refreshTokenCookieDomain: String?, + val refreshTokenCookiePath: String = REFRESH_TOKEN_COOKIE_PATH, + val refreshTokenCookieSecure: Boolean = true, + val refreshTokenCookieHttpOnly: Boolean = true, + val email: String?, + val password: String?, + val secondFactorCode: String?, + val initialSyncCompleted: Boolean, + val isE2EIRequired: Boolean, + val clientId: String?, +) { + companion object { + const val REFRESH_TOKEN_COOKIE_NAME = "zuid" + const val REFRESH_TOKEN_COOKIE_PATH = "/" + } +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/CoroutineScopeExt.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/CoroutineScopeExt.kt new file mode 100644 index 00000000000..af88a2e8ac8 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/CoroutineScopeExt.kt @@ -0,0 +1,25 @@ +/* + * 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.shared.auth + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job + +internal fun CoroutineScope.cancelScope() { + coroutineContext[Job]?.cancel() +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/NoEffect.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/NoEffect.kt new file mode 100644 index 00000000000..3d092db7cec --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/NoEffect.kt @@ -0,0 +1,20 @@ +/* + * 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.shared.auth + +data object NoEffect diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/SharedAuthConfig.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/SharedAuthConfig.kt new file mode 100644 index 00000000000..3df01be48de --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/SharedAuthConfig.kt @@ -0,0 +1,29 @@ +/* + * 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.shared.auth + +import com.wire.shared.auth.login.model.LoginServerLinks + +data class SharedAuthConfig( + val defaultServerLinks: LoginServerLinks, + val isThereActiveSession: Boolean = false, + val maxAccountsReached: Boolean = false, + val nomadAccountBlocksLogin: Boolean = false, + val isAccountCreationAllowed: Boolean = true, + val useNewRegistration: Boolean = true, +) diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/SharedViewModel.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/SharedViewModel.kt new file mode 100644 index 00000000000..dcca5aa2fc7 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/SharedViewModel.kt @@ -0,0 +1,99 @@ +/* + * 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.shared.auth + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +interface SharedCloseable { + fun close() +} + +/** + * Platform-neutral shared ViewModel contract. + * + * Android can bind [state] and [effects] directly from Compose. Other platform export modules + * should wrap this type in platform-friendly concrete wrappers instead of duplicating auth logic. + */ +class SharedViewModel( + val state: StateFlow, + val effects: Flow, + private val onIntent: (Intent) -> Unit, + private val onClose: () -> Unit = {}, +) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private var isClosed = false + + val currentState: State + get() = state.value + + fun observeState(observer: (State) -> Unit): SharedCloseable = + observe(state, observer) + + fun observeEffect(observer: (Effect) -> Unit): SharedCloseable = + observe(effects, observer) + + fun sendIntent(intent: Intent) { + onIntent(intent) + } + + fun close() { + if (isClosed) return + isClosed = true + scope.close() + onClose() + } + + private fun observe( + flow: Flow, + observer: (T) -> Unit, + ): SharedCloseable { + val job = scope.launch { + flow.collect(observer) + } + return job.asSharedCloseable() + } +} + +interface SharedObservableViewModel { + val currentState: State + + fun observeState(observer: (State) -> Unit): SharedCloseable + + fun observeEffect(observer: (Effect) -> Unit): SharedCloseable + + fun sendIntent(intent: Intent) + + fun close() +} + +private fun CoroutineScope.close() { + coroutineContext[Job]?.cancel() +} + +private fun Job.asSharedCloseable(): SharedCloseable = + object : SharedCloseable { + override fun close() { + cancel() + } + } diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailContract.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailContract.kt new file mode 100644 index 00000000000..dccf9858f25 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailContract.kt @@ -0,0 +1,130 @@ +/* + * 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.shared.auth.email + +data class LoginEmailState( + val userIdentifier: String = "", + val password: String = "", + val proxyIdentifier: String = "", + val proxyPassword: String = "", + val userIdentifierEnabled: Boolean = true, + val loginEnabled: Boolean = false, + val flowState: LoginEmailFlowState = LoginEmailFlowState.Default, + val secondFactorVerificationCode: LoginEmailVerificationCodeState = LoginEmailVerificationCodeState(), +) { + val isPasswordEmpty: Boolean + get() = password.isEmpty() + + val isPasswordNotEmpty: Boolean + get() = password.isNotEmpty() + + val isLoading: Boolean + get() = flowState is LoginEmailFlowState.Loading + + val isSuccess: Boolean + get() = flowState is LoginEmailFlowState.Success + + val isInvalidCredentials: Boolean + get() = flowState is LoginEmailFlowState.Error && flowState.type is LoginEmailError.InvalidCredentials + + val isSecondFactorRequired: Boolean + get() = secondFactorVerificationCode.isCodeInputNecessary + + val isSecondFactorInvalid: Boolean + get() = secondFactorVerificationCode.isCurrentCodeInvalid + + val secondFactorCode: String + get() = secondFactorVerificationCode.code + + val isSecondFactorCodeComplete: Boolean + get() = secondFactorVerificationCode.code.length == secondFactorVerificationCode.codeLength + + val secondFactorEmail: String + get() = secondFactorVerificationCode.emailUsed +} + +data class LoginEmailVerificationCodeState( + val code: String = "", + val codeLength: Int = DEFAULT_VERIFICATION_CODE_LENGTH, + val emailUsed: String = "", + val isCodeInputNecessary: Boolean = false, + val isCurrentCodeInvalid: Boolean = false, + val remainingTimerText: String? = null, +) { + companion object { + const val DEFAULT_VERIFICATION_CODE_LENGTH = 6 + } +} + +sealed interface LoginEmailFlowState { + data object Default : LoginEmailFlowState + data object Loading : LoginEmailFlowState + data object Canceled : LoginEmailFlowState + data class Success( + val initialSyncCompleted: Boolean, + val isE2EIRequired: Boolean, + ) : LoginEmailFlowState + data class Error(val type: LoginEmailError) : LoginEmailFlowState +} + +sealed interface LoginEmailError { + data object InvalidUserIdentifier : LoginEmailError + data object InvalidCredentials : LoginEmailError + data object ProxyAuthenticationFailed : LoginEmailError + data object UserAlreadyExists : LoginEmailError + data object PasswordNeededToRegisterClient : LoginEmailError + data object RequestSecondFactorWithHandle : LoginEmailError + data object ServerVersionNotSupported : LoginEmailError + data object ClientUpdateRequired : LoginEmailError + data object AccountSuspended : LoginEmailError + data object AccountPendingActivation : LoginEmailError + data object TooManyDevices : LoginEmailError + data class Generic(val message: String? = null) : LoginEmailError +} + +sealed interface LoginEmailIntent { + data class UserIdentifierChanged(val value: String) : LoginEmailIntent + + data class PasswordChanged(val value: String) : LoginEmailIntent + + data class ProxyIdentifierChanged(val value: String) : LoginEmailIntent + data class ProxyPasswordChanged(val value: String) : LoginEmailIntent + + data class SecondFactorCodeChanged(val value: String) : LoginEmailIntent + + data class SubmitLogin(val usernameAllowed: Boolean = true) : LoginEmailIntent + + data object ClearLoginErrors : LoginEmailIntent + data object CancelLogin : LoginEmailIntent + data object SecondFactorBackPressed : LoginEmailIntent + data object ResendSecondFactorCode : LoginEmailIntent +} + +/** + * One-shot login actions for platform hosts. + * + * Invalid credentials intentionally do not emit an effect. They are represented only through + * [LoginEmailFlowState.Error] with [LoginEmailError.InvalidCredentials]. + */ +sealed interface LoginEmailEffect { + data class LoginSucceeded( + val initialSyncCompleted: Boolean, + val isE2EIRequired: Boolean, + ) : LoginEmailEffect + data object RemoveDeviceNeeded : LoginEmailEffect +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailGateway.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailGateway.kt new file mode 100644 index 00000000000..44c1e8ce71e --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailGateway.kt @@ -0,0 +1,85 @@ +/* + * 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.shared.auth.email + +import com.wire.shared.auth.AuthLoginSuccessPayload + +/** + * Platform boundary for email login. + * + * This keeps the shared ViewModel free from iOS/Android runtime setup while the implementation still + * delegates to Kalium. Remove this boundary once the shared auth module can depend on a stable Kalium + * login orchestration API directly and both Android and iOS create the same shared ViewModel from that graph. + */ +interface LoginEmailGateway { + suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailGatewayResult + + suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailGatewayResult +} + +sealed interface LoginEmailGatewayResult { + data class Success( + val initialSyncCompleted: Boolean, + val isE2EIRequired: Boolean, + val payload: AuthLoginSuccessPayload, + ) : LoginEmailGatewayResult + + data class SecondFactorRequired( + val email: String, + val isCurrentCodeInvalid: Boolean = false, + ) : LoginEmailGatewayResult + + data object RemoveDeviceNeeded : LoginEmailGatewayResult + data class Failure(val error: LoginEmailError) : LoginEmailGatewayResult +} + +class LocalLoginEmailGateway : LoginEmailGateway { + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailGatewayResult = + LoginEmailGatewayResult.Success( + initialSyncCompleted = false, + isE2EIRequired = false, + payload = AuthLoginSuccessPayload( + userIdValue = "", + userIdDomain = null, + accessTokenValue = "", + accessTokenType = "", + accessTokenExpiresInSeconds = null, + refreshTokenValue = "", + refreshTokenCookieDomain = null, + email = userIdentifier, + password = password, + secondFactorCode = secondFactorVerificationCode, + initialSyncCompleted = false, + isE2EIRequired = false, + clientId = null, + ), + ) + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailGatewayResult = + LoginEmailGatewayResult.SecondFactorRequired(email = userIdentifier) +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailViewModelFactory.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailViewModelFactory.kt new file mode 100644 index 00000000000..64cea12840a --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailViewModelFactory.kt @@ -0,0 +1,253 @@ +/* + * 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.shared.auth.email + +import com.wire.shared.auth.SharedViewModel +import com.wire.shared.auth.cancelScope +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +@Inject +class LoginEmailViewModelFactory( + private val gateway: LoginEmailGateway, +) { + fun create( + userIdentifier: String = "", + coroutineContext: CoroutineContext = Dispatchers.Unconfined, + ): SharedViewModel { + val scope = CoroutineScope(SupervisorJob() + coroutineContext) + val state = MutableStateFlow( + LoginEmailState( + userIdentifier = userIdentifier, + userIdentifierEnabled = userIdentifier.isBlank(), + loginEnabled = false, + ) + ) + val effects = MutableSharedFlow(extraBufferCapacity = 1) + + return SharedViewModel( + state = state.asStateFlow(), + effects = effects.asSharedFlow(), + onIntent = { intent -> handleIntent(intent, state, effects, scope) }, + onClose = { scope.cancelScope() }, + ) + } + + private fun handleIntent( + intent: LoginEmailIntent, + state: MutableStateFlow, + effects: MutableSharedFlow, + scope: CoroutineScope, + ) { + when (intent) { + is LoginEmailIntent.UserIdentifierChanged -> onUserIdentifierChanged(state, intent.value) + is LoginEmailIntent.PasswordChanged -> onPasswordChanged(state, intent.value) + is LoginEmailIntent.ProxyIdentifierChanged -> state.update { it.copy(proxyIdentifier = intent.value) } + is LoginEmailIntent.ProxyPasswordChanged -> state.update { it.copy(proxyPassword = intent.value) } + is LoginEmailIntent.SecondFactorCodeChanged -> onSecondFactorCodeChanged(state, intent.value) + is LoginEmailIntent.SubmitLogin -> submitLogin(state, effects, scope, intent.usernameAllowed) + LoginEmailIntent.ClearLoginErrors -> state.update { it.copy(flowState = LoginEmailFlowState.Default) } + LoginEmailIntent.CancelLogin -> state.update { it.copy(flowState = LoginEmailFlowState.Canceled) } + LoginEmailIntent.SecondFactorBackPressed -> onSecondFactorBackPressed(state) + LoginEmailIntent.ResendSecondFactorCode -> requestSecondFactorCode(state, scope) + } + } + + private fun onUserIdentifierChanged( + state: MutableStateFlow, + value: String, + ) { + state.update { + it.copy( + userIdentifier = value, + loginEnabled = it.canSubmit(userIdentifier = value), + flowState = LoginEmailFlowState.Default, + ) + } + } + + private fun onPasswordChanged( + state: MutableStateFlow, + value: String, + ) { + state.update { + it.copy( + password = value, + loginEnabled = it.canSubmit(password = value), + flowState = LoginEmailFlowState.Default, + ) + } + } + + private fun onSecondFactorCodeChanged( + state: MutableStateFlow, + value: String, + ) { + state.update { + it.copy( + secondFactorVerificationCode = it.secondFactorVerificationCode.copy( + code = value, + isCurrentCodeInvalid = false, + ) + ) + } + } + + private fun onSecondFactorBackPressed(state: MutableStateFlow) { + state.update { + it.copy( + secondFactorVerificationCode = LoginEmailVerificationCodeState(), + flowState = LoginEmailFlowState.Default, + ) + } + } + + private fun submitLogin( + state: MutableStateFlow, + effects: MutableSharedFlow, + scope: CoroutineScope, + usernameAllowed: Boolean, + ) { + val currentState = state.value + if (!currentState.canSubmit()) { + state.update { it.copy(flowState = LoginEmailFlowState.Error(LoginEmailError.InvalidCredentials)) } + return + } + state.update { it.copy(flowState = LoginEmailFlowState.Loading, loginEnabled = false) } + scope.launch { + handleLoginResult( + result = gateway.login( + userIdentifier = currentState.userIdentifier, + password = currentState.password, + secondFactorVerificationCode = currentState.secondFactorVerificationCode.code.takeIf { it.isNotBlank() }, + usernameAllowed = usernameAllowed, + ), + state = state, + effects = effects, + ) + } + } + + private fun handleLoginResult( + result: LoginEmailGatewayResult, + state: MutableStateFlow, + effects: MutableSharedFlow, + ) { + when (result) { + is LoginEmailGatewayResult.Failure -> setLoginError(state, result.error) + LoginEmailGatewayResult.RemoveDeviceNeeded -> { + effects.tryEmit(LoginEmailEffect.RemoveDeviceNeeded) + setLoginError(state, LoginEmailError.TooManyDevices) + } + is LoginEmailGatewayResult.SecondFactorRequired -> setSecondFactorRequired(state, result) + is LoginEmailGatewayResult.Success -> setLoginSuccess(state, effects, result) + } + } + + private fun setLoginError( + state: MutableStateFlow, + error: LoginEmailError, + ) { + state.update { + val flowState = LoginEmailFlowState.Error(error) + it.copy( + flowState = flowState, + loginEnabled = it.canSubmit(flowState = flowState), + ) + } + } + + private fun setSecondFactorRequired( + state: MutableStateFlow, + result: LoginEmailGatewayResult.SecondFactorRequired, + ) { + state.update { + it.copy( + flowState = LoginEmailFlowState.Default, + loginEnabled = it.canSubmit(flowState = LoginEmailFlowState.Default), + secondFactorVerificationCode = it.secondFactorVerificationCode.copy( + isCodeInputNecessary = true, + emailUsed = result.email, + isCurrentCodeInvalid = result.isCurrentCodeInvalid, + ), + ) + } + } + + private fun setLoginSuccess( + state: MutableStateFlow, + effects: MutableSharedFlow, + result: LoginEmailGatewayResult.Success, + ) { + val flowState = LoginEmailFlowState.Success( + initialSyncCompleted = result.initialSyncCompleted, + isE2EIRequired = result.isE2EIRequired, + ) + state.update { it.copy(flowState = flowState, loginEnabled = false) } + effects.tryEmit( + LoginEmailEffect.LoginSucceeded( + initialSyncCompleted = result.initialSyncCompleted, + isE2EIRequired = result.isE2EIRequired, + ) + ) + } + + private fun requestSecondFactorCode( + state: MutableStateFlow, + scope: CoroutineScope, + ) { + scope.launch { + when (val result = gateway.requestSecondFactorCode(state.value.userIdentifier)) { + is LoginEmailGatewayResult.Failure -> + state.update { it.copy(flowState = LoginEmailFlowState.Error(result.error)) } + + is LoginEmailGatewayResult.SecondFactorRequired -> + state.update { + it.copy( + flowState = LoginEmailFlowState.Default, + secondFactorVerificationCode = it.secondFactorVerificationCode.copy( + isCodeInputNecessary = true, + emailUsed = result.email, + isCurrentCodeInvalid = result.isCurrentCodeInvalid, + ), + ) + } + + LoginEmailGatewayResult.RemoveDeviceNeeded, + is LoginEmailGatewayResult.Success -> + Unit + } + } + } +} + +private fun LoginEmailState.canSubmit( + userIdentifier: String = this.userIdentifier, + password: String = this.password, + flowState: LoginEmailFlowState = this.flowState, +): Boolean = + userIdentifier.isNotBlank() && password.isNotBlank() && flowState !is LoginEmailFlowState.Loading diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowBackend.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowBackend.kt new file mode 100644 index 00000000000..9d1e47c5a2b --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowBackend.kt @@ -0,0 +1,98 @@ +/* + * 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.shared.auth.flow + +import com.wire.shared.auth.AuthLoginSuccessPayload + +/** + * Backend boundary for the shared auth flow coordinator. + * + * The common ViewModel owns UI flow state only. The real Kalium-backed implementation should + * live in a platform source set and map Kalium results to these backend-agnostic results. + */ +interface AuthLoginFlowBackend { + suspend fun resolveIdentifier(identifier: String): AuthLoginFlowIdentifierResult + + suspend fun initiateSso(ssoCode: String): AuthLoginFlowIdentifierResult + + suspend fun loginWithEmail( + identifier: String, + password: String, + secondFactorCode: String?, + usernameAllowed: Boolean, + ): AuthLoginFlowLoginResult +} + +sealed interface AuthLoginFlowIdentifierResult { + data class EmailCredentialsRequired(val identifier: String) : AuthLoginFlowIdentifierResult + data class OpenSso(val url: String, val userIdentifier: String) : AuthLoginFlowIdentifierResult + data class Failure(val error: AuthLoginFlowError) : AuthLoginFlowIdentifierResult +} + +sealed interface AuthLoginFlowLoginResult { + data class Success( + val initialSyncCompleted: Boolean, + val isE2EIRequired: Boolean, + val payload: AuthLoginSuccessPayload, + ) : AuthLoginFlowLoginResult + + data class SecondFactorRequired( + val email: String, + val isCurrentCodeInvalid: Boolean = false, + ) : AuthLoginFlowLoginResult + + data object RemoveDeviceNeeded : AuthLoginFlowLoginResult + data class Failure(val error: AuthLoginFlowError) : AuthLoginFlowLoginResult +} + +class LocalAuthLoginFlowBackend : AuthLoginFlowBackend { + override suspend fun resolveIdentifier(identifier: String): AuthLoginFlowIdentifierResult = + AuthLoginFlowIdentifierResult.EmailCredentialsRequired(identifier) + + override suspend fun initiateSso(ssoCode: String): AuthLoginFlowIdentifierResult = + AuthLoginFlowIdentifierResult.OpenSso( + url = "", + userIdentifier = ssoCode, + ) + + override suspend fun loginWithEmail( + identifier: String, + password: String, + secondFactorCode: String?, + usernameAllowed: Boolean, + ): AuthLoginFlowLoginResult = + AuthLoginFlowLoginResult.Success( + initialSyncCompleted = false, + isE2EIRequired = false, + payload = AuthLoginSuccessPayload( + userIdValue = "", + userIdDomain = null, + accessTokenValue = "", + accessTokenType = "", + accessTokenExpiresInSeconds = null, + refreshTokenValue = "", + refreshTokenCookieDomain = null, + email = identifier, + password = password, + secondFactorCode = secondFactorCode, + initialSyncCompleted = false, + isE2EIRequired = false, + clientId = null, + ), + ) +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowContract.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowContract.kt new file mode 100644 index 00000000000..e51325ab337 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowContract.kt @@ -0,0 +1,89 @@ +/* + * 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.shared.auth.flow + +import com.wire.shared.auth.AuthLoginSuccessPayload + +data class AuthLoginFlowState( + val step: AuthLoginFlowStep = AuthLoginFlowStep.IdentifierEntry, + val identifier: String = "", + val ssoCode: String = "", + val password: String = "", + val secondFactorCode: String = "", + val secondFactorEmail: String = "", + val isLoading: Boolean = false, + val error: AuthLoginFlowError? = null, +) { + val canSubmitIdentifier: Boolean + get() = identifier.isNotBlank() && !isLoading + + val canSubmitSsoCode: Boolean + get() = ssoCode.isNotBlank() && !isLoading + + val canSubmitCredentials: Boolean + get() = identifier.isNotBlank() && password.isNotBlank() && !isLoading + + val canSubmitSecondFactor: Boolean + get() = secondFactorCode.isNotBlank() && !isLoading + + val isSuccess: Boolean + get() = step is AuthLoginFlowStep.Success +} + +sealed interface AuthLoginFlowStep { + data object IdentifierEntry : AuthLoginFlowStep + data object EmailCredentialsEntry : AuthLoginFlowStep + data object SecondFactorEntry : AuthLoginFlowStep + data class Success( + val initialSyncCompleted: Boolean, + val isE2EIRequired: Boolean, + ) : AuthLoginFlowStep +} + +sealed interface AuthLoginFlowError { + data object InvalidIdentifier : AuthLoginFlowError + data object InvalidCredentials : AuthLoginFlowError + data object InvalidSecondFactorCode : AuthLoginFlowError + data object TooManyDevices : AuthLoginFlowError + data class Generic(val message: String? = null) : AuthLoginFlowError +} + +sealed interface AuthLoginFlowIntent { + data class IdentifierChanged(val value: String) : AuthLoginFlowIntent + data class SsoCodeChanged(val value: String) : AuthLoginFlowIntent + data object SubmitIdentifier : AuthLoginFlowIntent + data object SubmitSsoCode : AuthLoginFlowIntent + data class PasswordChanged(val value: String) : AuthLoginFlowIntent + data class SubmitCredentials(val usernameAllowed: Boolean = true) : AuthLoginFlowIntent + data class SecondFactorCodeChanged(val value: String) : AuthLoginFlowIntent + data class SubmitSecondFactor(val usernameAllowed: Boolean = true) : AuthLoginFlowIntent + data object Back : AuthLoginFlowIntent + data object Cancel : AuthLoginFlowIntent + data object ClearError : AuthLoginFlowIntent +} + +sealed interface AuthLoginFlowEffect { + data class OpenSsoUrl( + val url: String, + val userIdentifier: String, + ) : AuthLoginFlowEffect + + data class LoginSucceeded( + val payload: AuthLoginSuccessPayload, + ) : AuthLoginFlowEffect +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowViewModelFactory.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowViewModelFactory.kt new file mode 100644 index 00000000000..6ac0dc9785e --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowViewModelFactory.kt @@ -0,0 +1,269 @@ +/* + * 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.shared.auth.flow + +import com.wire.shared.auth.SharedViewModel +import com.wire.shared.auth.cancelScope +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +@Inject +class AuthLoginFlowViewModelFactory( + private val backend: AuthLoginFlowBackend, +) { + fun create( + coroutineContext: CoroutineContext = Dispatchers.Unconfined, + ): SharedViewModel { + val scope = CoroutineScope(SupervisorJob() + coroutineContext) + val state = MutableStateFlow(AuthLoginFlowState()) + val effects = MutableSharedFlow(extraBufferCapacity = 1) + + return SharedViewModel( + state = state.asStateFlow(), + effects = effects.asSharedFlow(), + onIntent = { intent -> + when (intent) { + is AuthLoginFlowIntent.IdentifierChanged -> + state.update { it.copy(identifier = intent.value, error = null) } + + is AuthLoginFlowIntent.SsoCodeChanged -> + state.update { it.copy(ssoCode = intent.value, error = null) } + + AuthLoginFlowIntent.SubmitIdentifier -> + submitIdentifier(state, effects, scope) + + AuthLoginFlowIntent.SubmitSsoCode -> + submitSsoCode(state, effects, scope) + + is AuthLoginFlowIntent.PasswordChanged -> + state.update { it.copy(password = intent.value, error = null) } + + is AuthLoginFlowIntent.SubmitCredentials -> + submitCredentials(state, effects, scope, intent.usernameAllowed) + + is AuthLoginFlowIntent.SecondFactorCodeChanged -> + state.update { it.copy(secondFactorCode = intent.value, error = null) } + + is AuthLoginFlowIntent.SubmitSecondFactor -> + submitSecondFactor(state, effects, scope, intent.usernameAllowed) + + AuthLoginFlowIntent.Back -> + state.update { it.back() } + + AuthLoginFlowIntent.Cancel -> + state.update { AuthLoginFlowState() } + + AuthLoginFlowIntent.ClearError -> + state.update { it.copy(error = null) } + } + }, + onClose = { scope.cancelScope() }, + ) + } + + private fun submitIdentifier( + state: MutableStateFlow, + effects: MutableSharedFlow, + scope: CoroutineScope, + ) { + val identifier = state.value.identifier.trim() + if (identifier.isBlank()) { + state.update { it.copy(error = AuthLoginFlowError.InvalidIdentifier) } + return + } + state.update { it.copy(identifier = identifier, isLoading = true, error = null) } + scope.launch { + handleIdentifierResult( + result = backend.resolveIdentifier(identifier), + state = state, + effects = effects, + ) + } + } + + private fun submitSsoCode( + state: MutableStateFlow, + effects: MutableSharedFlow, + scope: CoroutineScope, + ) { + val ssoCode = state.value.ssoCode.trim() + if (ssoCode.isBlank()) { + state.update { it.copy(error = AuthLoginFlowError.InvalidIdentifier) } + return + } + state.update { it.copy(ssoCode = ssoCode, isLoading = true, error = null) } + scope.launch { + handleIdentifierResult( + result = backend.initiateSso(ssoCode), + state = state, + effects = effects, + ) + } + } + + private fun submitCredentials( + state: MutableStateFlow, + effects: MutableSharedFlow, + scope: CoroutineScope, + usernameAllowed: Boolean, + ) { + val currentState = state.value + if (!currentState.canSubmitCredentials) { + state.update { it.copy(error = AuthLoginFlowError.InvalidCredentials) } + return + } + state.update { it.copy(isLoading = true, error = null) } + scope.launch { + handleLoginResult( + result = backend.loginWithEmail( + identifier = currentState.identifier, + password = currentState.password, + secondFactorCode = null, + usernameAllowed = usernameAllowed, + ), + state = state, + effects = effects, + ) + } + } + + private fun submitSecondFactor( + state: MutableStateFlow, + effects: MutableSharedFlow, + scope: CoroutineScope, + usernameAllowed: Boolean, + ) { + val currentState = state.value + if (!currentState.canSubmitSecondFactor) { + state.update { it.copy(error = AuthLoginFlowError.InvalidSecondFactorCode) } + return + } + state.update { it.copy(isLoading = true, error = null) } + scope.launch { + handleLoginResult( + result = backend.loginWithEmail( + identifier = currentState.identifier, + password = currentState.password, + secondFactorCode = currentState.secondFactorCode, + usernameAllowed = usernameAllowed, + ), + state = state, + effects = effects, + ) + } + } + + private fun handleIdentifierResult( + result: AuthLoginFlowIdentifierResult, + state: MutableStateFlow, + effects: MutableSharedFlow, + ) { + when (result) { + is AuthLoginFlowIdentifierResult.EmailCredentialsRequired -> + state.update { + it.copy( + step = AuthLoginFlowStep.EmailCredentialsEntry, + identifier = result.identifier, + isLoading = false, + ) + } + + is AuthLoginFlowIdentifierResult.Failure -> + state.update { it.copy(isLoading = false, error = result.error) } + + is AuthLoginFlowIdentifierResult.OpenSso -> { + effects.tryEmit(AuthLoginFlowEffect.OpenSsoUrl(result.url, result.userIdentifier)) + state.update { it.copy(isLoading = false) } + } + } + } + + private fun handleLoginResult( + result: AuthLoginFlowLoginResult, + state: MutableStateFlow, + effects: MutableSharedFlow, + ) { + when (result) { + is AuthLoginFlowLoginResult.Failure -> + state.update { it.copy(isLoading = false, error = result.error) } + + AuthLoginFlowLoginResult.RemoveDeviceNeeded -> + state.update { it.copy(isLoading = false, error = AuthLoginFlowError.TooManyDevices) } + + is AuthLoginFlowLoginResult.SecondFactorRequired -> + state.update { + it.copy( + step = AuthLoginFlowStep.SecondFactorEntry, + secondFactorEmail = result.email, + secondFactorCode = if (result.isCurrentCodeInvalid) it.secondFactorCode else "", + isLoading = false, + error = if (result.isCurrentCodeInvalid) AuthLoginFlowError.InvalidSecondFactorCode else null, + ) + } + + is AuthLoginFlowLoginResult.Success -> { + effects.tryEmit(AuthLoginFlowEffect.LoginSucceeded(result.payload)) + state.update { + it.copy( + step = AuthLoginFlowStep.Success( + initialSyncCompleted = result.initialSyncCompleted, + isE2EIRequired = result.isE2EIRequired, + ), + isLoading = false, + error = null, + ) + } + } + } + } +} + +private fun AuthLoginFlowState.back(): AuthLoginFlowState = + when (step) { + AuthLoginFlowStep.IdentifierEntry -> + this + + AuthLoginFlowStep.EmailCredentialsEntry -> + copy( + step = AuthLoginFlowStep.IdentifierEntry, + password = "", + isLoading = false, + error = null, + ) + + AuthLoginFlowStep.SecondFactorEntry -> + copy( + step = AuthLoginFlowStep.EmailCredentialsEntry, + secondFactorCode = "", + secondFactorEmail = "", + isLoading = false, + error = null, + ) + + is AuthLoginFlowStep.Success -> + this + } diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavArgs.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavArgs.kt new file mode 100644 index 00000000000..789ab183759 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavArgs.kt @@ -0,0 +1,60 @@ +/* + * 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.shared.auth.login.model + +data class LoginNavArgs( + val userIdentifier: LoginUserIdentifier = LoginUserIdentifier.None, + val ssoLoginResult: LoginSsoResult? = null, + val passwordPath: LoginPasswordPath? = null, + val ssoCodeAutoLogin: LoginSsoCodeAutoLogin? = null, +) + +sealed interface LoginUserIdentifier { + val userIdentifierEditable: Boolean + + data object None : LoginUserIdentifier { + override val userIdentifierEditable: Boolean = true + } + + data class PreFilled( + val value: String, + val editable: Boolean = false, + ) : LoginUserIdentifier { + override val userIdentifierEditable: Boolean = editable + } +} + +data class LoginSsoCodeAutoLogin( + val ssoCode: String, + val autoInitiateLogin: Boolean = true, + val nomadServiceUrl: String? = null, + val cookieLabel: String? = null, +) + +data class LoginSsoResult( + val success: Boolean, + val cookie: String? = null, + val error: LoginSsoFailure? = null, +) + +enum class LoginSsoFailure { + InvalidRequest, + InvalidCookie, + ServerError, + Unknown, +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavigationCommand.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavigationCommand.kt new file mode 100644 index 00000000000..795c9b18f70 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavigationCommand.kt @@ -0,0 +1,39 @@ +/* + * 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.shared.auth.login.model + +fun interface LoginNavigator { + fun navigate(command: LoginNavigationCommand) +} + +sealed interface LoginNavigationCommand { + data class EnterpriseLoginNotSupported(val userIdentifier: String) : LoginNavigationCommand + data class EmailPassword(val userIdentifier: String, val passwordPath: LoginPasswordPath) : LoginNavigationCommand + data class CustomConfig(val userIdentifier: String, val customServerLinks: LoginServerLinks) : LoginNavigationCommand + data class Sso(val url: String, val config: LoginSsoUrlConfig) : LoginNavigationCommand + data class Success(val nextStep: LoginSuccessNextStep) : LoginNavigationCommand +} + +data class LoginSsoUrlConfig(val userIdentifier: String = "") + +enum class LoginSuccessNextStep { + E2EIEnrollment, + InitialSync, + TooManyDevices, + None, +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginPasswordPath.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginPasswordPath.kt new file mode 100644 index 00000000000..f497c022ea3 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginPasswordPath.kt @@ -0,0 +1,29 @@ +/* + * 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.shared.auth.login.model + +data class LoginPasswordPath( + val customServerLinks: LoginServerLinks? = null, + val isCloudAccountCreationPossible: Boolean? = null, + val domainClaimedByOrg: LoginDomainClaimedByOrg = LoginDomainClaimedByOrg.NotClaimed, +) + +sealed interface LoginDomainClaimedByOrg { + data object NotClaimed : LoginDomainClaimedByOrg + data class Claimed(val domain: String) : LoginDomainClaimedByOrg +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginScreenState.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginScreenState.kt new file mode 100644 index 00000000000..64a7c2f717b --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginScreenState.kt @@ -0,0 +1,43 @@ +/* + * 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.shared.auth.login.model + +data class LoginScreenState( + val isThereActiveSession: Boolean = false, + val userIdentifierEnabled: Boolean = true, + val nextEnabled: Boolean = false, + val flowState: LoginFlowState = LoginFlowState.Default, +) + +sealed interface LoginFlowState { + data object Default : LoginFlowState + data object Loading : LoginFlowState + data class CustomConfigDialog(val serverLinks: LoginServerLinks) : LoginFlowState + data class Error(val error: LoginError) : LoginFlowState +} + +sealed interface LoginError { + data object InvalidValue : LoginError + data object ServerVersionNotSupported : LoginError + data object ClientUpdateRequired : LoginError + data class SsoResultFailure(val result: LoginSsoFailure) : LoginError + data object InvalidSsoCode : LoginError + data object InvalidSsoCookie : LoginError + data object UserAlreadyExists : LoginError + data object GenericError : LoginError +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginServerLinks.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginServerLinks.kt new file mode 100644 index 00000000000..a814cbfd9f0 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginServerLinks.kt @@ -0,0 +1,39 @@ +/* + * 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.shared.auth.login.model + +data class LoginServerLinks( + val api: String, + val accounts: String, + val webSocket: String, + val blackList: String, + val teams: String, + val website: String, + val title: String, + val isOnPremises: Boolean, + val apiProxy: LoginApiProxy? = null, +) { + val isProxyEnabled: Boolean + get() = apiProxy != null +} + +data class LoginApiProxy( + val needsAuthentication: Boolean, + val host: String, + val port: Int, +) diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierBackend.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierBackend.kt new file mode 100644 index 00000000000..3918e0074ee --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierBackend.kt @@ -0,0 +1,65 @@ +/* + * 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.shared.auth.newlogin + +import com.wire.shared.auth.login.model.LoginServerLinks + +interface NewLoginIdentifierBackend { + suspend fun resolveEmail(userIdentifier: String): NewLoginIdentifierBackendResult + + suspend fun initiateSso(ssoCode: String): NewLoginIdentifierBackendResult +} + +sealed interface NewLoginIdentifierBackendResult { + data class OpenEmailPassword( + val userIdentifier: String, + val path: NewLoginPasswordPath, + ) : NewLoginIdentifierBackendResult + + data class OpenCustomConfig( + val userIdentifier: String, + val serverLinks: LoginServerLinks, + ) : NewLoginIdentifierBackendResult + + data class OpenSso( + val url: String, + val config: NewLoginSsoUrlConfig, + ) : NewLoginIdentifierBackendResult + + data class EnterpriseLoginNotSupported( + val userIdentifier: String, + ) : NewLoginIdentifierBackendResult + + data class Error( + val error: NewLoginIdentifierDialogError, + ) : NewLoginIdentifierBackendResult +} + +class LocalNewLoginIdentifierBackend : NewLoginIdentifierBackend { + override suspend fun resolveEmail(userIdentifier: String): NewLoginIdentifierBackendResult = + NewLoginIdentifierBackendResult.OpenEmailPassword( + userIdentifier = userIdentifier, + path = NewLoginPasswordPath(), + ) + + override suspend fun initiateSso(ssoCode: String): NewLoginIdentifierBackendResult = + NewLoginIdentifierBackendResult.OpenSso( + url = "", + config = NewLoginSsoUrlConfig(userIdentifier = ssoCode), + ) +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierContract.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierContract.kt new file mode 100644 index 00000000000..290991f9c40 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierContract.kt @@ -0,0 +1,128 @@ +/* + * 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.shared.auth.newlogin + +import com.wire.shared.auth.login.model.LoginServerLinks + +/** + * Platform-neutral state for the first new-login screen where the user enters an email or SSO code. + */ +data class NewLoginIdentifierState( + val userIdentifier: String = "", + val isThereActiveSession: Boolean = false, + val userIdentifierEnabled: Boolean = true, + val nextEnabled: Boolean = false, + val flowState: NewLoginIdentifierFlowState = NewLoginIdentifierFlowState.Default, +) { + val isLoading: Boolean + get() = flowState is NewLoginIdentifierFlowState.Loading + + val isCustomConfigDialogVisible: Boolean + get() = flowState is NewLoginIdentifierFlowState.CustomConfigDialog + + val customConfigServerLinks: LoginServerLinks? + get() = (flowState as? NewLoginIdentifierFlowState.CustomConfigDialog)?.serverLinks + + val hasTextFieldError: Boolean + get() = flowState is NewLoginIdentifierFlowState.TextFieldError + + val textFieldError: NewLoginIdentifierTextFieldError? + get() = (flowState as? NewLoginIdentifierFlowState.TextFieldError)?.error + + val hasDialogError: Boolean + get() = flowState is NewLoginIdentifierFlowState.DialogError + + val dialogError: NewLoginIdentifierDialogError? + get() = (flowState as? NewLoginIdentifierFlowState.DialogError)?.error +} + +sealed interface NewLoginIdentifierFlowState { + data object Default : NewLoginIdentifierFlowState + data object Loading : NewLoginIdentifierFlowState + data class CustomConfigDialog(val serverLinks: LoginServerLinks) : NewLoginIdentifierFlowState + data class TextFieldError(val error: NewLoginIdentifierTextFieldError) : NewLoginIdentifierFlowState + data class DialogError(val error: NewLoginIdentifierDialogError) : NewLoginIdentifierFlowState +} + +enum class NewLoginIdentifierTextFieldError { + InvalidValue, +} + +sealed interface NewLoginIdentifierDialogError { + data object ServerVersionNotSupported : NewLoginIdentifierDialogError + data object ClientUpdateRequired : NewLoginIdentifierDialogError + data class SSOResultFailure(val code: NewLoginSsoFailureCode) : NewLoginIdentifierDialogError + data object InvalidSSOCode : NewLoginIdentifierDialogError + data object InvalidSSOCookie : NewLoginIdentifierDialogError + data object UserAlreadyExists : NewLoginIdentifierDialogError + data class GenericError(val message: String? = null) : NewLoginIdentifierDialogError +} + +enum class NewLoginSsoFailureCode { + Unknown, + InvalidCode, + InvalidCookie, + Cancelled, +} + +sealed interface NewLoginIdentifierIntent { + data class UserIdentifierChanged(val userIdentifier: String) : NewLoginIdentifierIntent + data object Submit : NewLoginIdentifierIntent + data object DismissDialog : NewLoginIdentifierIntent + data class ConfirmCustomServer(val serverLinks: LoginServerLinks) : NewLoginIdentifierIntent + data class SSOResultReceived(val result: NewLoginSsoResult) : NewLoginIdentifierIntent +} + +sealed interface NewLoginIdentifierEffect { + data class EnterpriseLoginNotSupported(val userIdentifier: String) : NewLoginIdentifierEffect + data class OpenEmailPassword(val userIdentifier: String, val path: NewLoginPasswordPath) : NewLoginIdentifierEffect + data class OpenCustomConfig(val userIdentifier: String, val serverLinks: LoginServerLinks) : NewLoginIdentifierEffect + data class OpenSSO(val url: String, val config: NewLoginSsoUrlConfig) : NewLoginIdentifierEffect + data class LoginSucceeded(val nextStep: NewLoginSuccessNextStep) : NewLoginIdentifierEffect +} + +enum class NewLoginSuccessNextStep { + E2EIEnrollment, + InitialSync, + TooManyDevices, + None, +} + +data class NewLoginPasswordPath( + val customServerConfig: LoginServerLinks? = null, + val isCloudAccountCreationPossible: Boolean = true, + val domainClaimedByOrg: NewLoginDomainClaimedByOrg = NewLoginDomainClaimedByOrg.NotClaimed, +) + +sealed interface NewLoginDomainClaimedByOrg { + data object NotClaimed : NewLoginDomainClaimedByOrg + data class Claimed(val domain: String) : NewLoginDomainClaimedByOrg +} + +data class NewLoginSsoUrlConfig( + val userIdentifier: String, +) + +sealed interface NewLoginSsoResult { + data class Success( + val cookie: String, + val serverConfigId: String, + ) : NewLoginSsoResult + + data class Failure(val code: NewLoginSsoFailureCode) : NewLoginSsoResult +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt new file mode 100644 index 00000000000..5bf0d2ecc83 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt @@ -0,0 +1,36 @@ +/* + * 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.shared.auth.newlogin + +fun NewLoginIdentifierState.withUserIdentifier(userIdentifier: String): NewLoginIdentifierState { + val newFlowState = when (flowState) { + is NewLoginIdentifierFlowState.TextFieldError -> NewLoginIdentifierFlowState.Default + else -> flowState + } + return copy( + userIdentifier = userIdentifier, + flowState = newFlowState, + nextEnabled = newFlowState !is NewLoginIdentifierFlowState.Loading && userIdentifier.isNotEmpty(), + ) +} + +fun NewLoginIdentifierState.withFlowState(flowState: NewLoginIdentifierFlowState): NewLoginIdentifierState = + copy( + flowState = flowState, + nextEnabled = flowState !is NewLoginIdentifierFlowState.Loading && userIdentifier.isNotEmpty(), + ) diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierViewModelFactory.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierViewModelFactory.kt new file mode 100644 index 00000000000..16bb4c096d2 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierViewModelFactory.kt @@ -0,0 +1,187 @@ +/* + * 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.shared.auth.newlogin + +import com.wire.shared.auth.SharedAuthConfig +import com.wire.shared.auth.SharedViewModel +import com.wire.shared.auth.cancelScope +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +@Inject +class NewLoginIdentifierViewModelFactory( + private val config: SharedAuthConfig, + private val backend: NewLoginIdentifierBackend, +) { + fun create( + coroutineContext: CoroutineContext = Dispatchers.Unconfined, + ): SharedViewModel { + val scope = CoroutineScope(SupervisorJob() + coroutineContext) + val state = MutableStateFlow( + NewLoginIdentifierState( + isThereActiveSession = config.isThereActiveSession, + ) + ) + val effects = MutableSharedFlow(extraBufferCapacity = 1) + + return SharedViewModel( + state = state.asStateFlow(), + effects = effects.asSharedFlow(), + onIntent = { intent -> + when (intent) { + is NewLoginIdentifierIntent.UserIdentifierChanged -> + state.update { it.withUserIdentifier(intent.userIdentifier) } + + NewLoginIdentifierIntent.Submit -> + submit(state, effects, scope) + + NewLoginIdentifierIntent.DismissDialog -> + state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } + + is NewLoginIdentifierIntent.ConfirmCustomServer -> { + effects.tryEmit( + NewLoginIdentifierEffect.OpenCustomConfig( + userIdentifier = state.value.userIdentifier, + serverLinks = intent.serverLinks, + ) + ) + state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } + } + + is NewLoginIdentifierIntent.SSOResultReceived -> + handleSsoResult(intent.result, state, effects) + } + }, + onClose = { scope.cancelScope() }, + ) + } + + private fun submit( + state: MutableStateFlow, + effects: MutableSharedFlow, + scope: CoroutineScope, + ) { + val userIdentifier = state.value.userIdentifier.trim() + when { + userIdentifier.isValidSsoCode() -> + resolveWithBackend(state, effects, scope) { backend.initiateSso(userIdentifier) } + + userIdentifier.isValidEmail() -> + resolveWithBackend(state, effects, scope) { backend.resolveEmail(userIdentifier) } + + else -> state.update { + it.withFlowState( + NewLoginIdentifierFlowState.TextFieldError(NewLoginIdentifierTextFieldError.InvalidValue) + ) + } + } + } + + private fun resolveWithBackend( + state: MutableStateFlow, + effects: MutableSharedFlow, + scope: CoroutineScope, + resolve: suspend () -> NewLoginIdentifierBackendResult, + ) { + state.update { it.withFlowState(NewLoginIdentifierFlowState.Loading) } + scope.launch { + when (val result = resolve()) { + is NewLoginIdentifierBackendResult.EnterpriseLoginNotSupported -> { + effects.tryEmit(NewLoginIdentifierEffect.EnterpriseLoginNotSupported(result.userIdentifier)) + state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } + } + + is NewLoginIdentifierBackendResult.Error -> + state.update { it.withFlowState(NewLoginIdentifierFlowState.DialogError(result.error)) } + + is NewLoginIdentifierBackendResult.OpenCustomConfig -> { + effects.tryEmit( + NewLoginIdentifierEffect.OpenCustomConfig( + userIdentifier = result.userIdentifier, + serverLinks = result.serverLinks, + ) + ) + state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } + } + + is NewLoginIdentifierBackendResult.OpenEmailPassword -> { + effects.tryEmit( + NewLoginIdentifierEffect.OpenEmailPassword( + userIdentifier = result.userIdentifier, + path = result.path, + ) + ) + state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } + } + + is NewLoginIdentifierBackendResult.OpenSso -> { + effects.tryEmit( + NewLoginIdentifierEffect.OpenSSO( + url = result.url, + config = result.config, + ) + ) + state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } + } + } + } + } + + private fun handleSsoResult( + result: NewLoginSsoResult, + state: MutableStateFlow, + effects: MutableSharedFlow, + ) { + when (result) { + is NewLoginSsoResult.Success -> { + effects.tryEmit(NewLoginIdentifierEffect.LoginSucceeded(NewLoginSuccessNextStep.None)) + state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } + } + + is NewLoginSsoResult.Failure -> + state.update { + it.withFlowState( + NewLoginIdentifierFlowState.DialogError( + NewLoginIdentifierDialogError.SSOResultFailure(result.code) + ) + ) + } + } + } +} + +private fun String.isValidEmail(): Boolean { + val atIndex = indexOf('@') + val dotIndex = lastIndexOf('.') + return atIndex > 0 && dotIndex > atIndex + 1 && dotIndex < lastIndex +} + +private fun String.isValidSsoCode(): Boolean = + startsWith(SSO_CODE_PREFIX) && removePrefix(SSO_CODE_PREFIX).matches(uuidRegex) + +private const val SSO_CODE_PREFIX = "wire-" +private val uuidRegex = Regex("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoBackend.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoBackend.kt new file mode 100644 index 00000000000..69da2571f42 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoBackend.kt @@ -0,0 +1,43 @@ +/* + * 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.shared.auth.sso + +import com.wire.shared.auth.login.model.LoginServerLinks + +interface LoginSsoBackend { + suspend fun initiateLogin(ssoCode: String): LoginSsoBackendResult + + suspend fun completeLogin( + cookie: String, + serverConfigId: String, + ): LoginSsoBackendResult +} + +sealed interface LoginSsoBackendResult { + data class OpenUrl( + val url: String, + val serverLinks: LoginServerLinks, + ) : LoginSsoBackendResult + + data class Success( + val initialSyncCompleted: Boolean, + val e2eiRequired: Boolean, + ) : LoginSsoBackendResult + + data class Error(val reason: LoginSsoError) : LoginSsoBackendResult +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoContract.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoContract.kt new file mode 100644 index 00000000000..73a3b2c32c9 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoContract.kt @@ -0,0 +1,106 @@ +/* + * 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.shared.auth.sso + +import com.wire.shared.auth.login.model.LoginServerLinks + +/** + * Platform-neutral state for the SSO login screen. + */ +data class LoginSsoState( + val ssoCode: String = "", + val loginEnabled: Boolean = false, + val flowState: LoginSsoFlowState = LoginSsoFlowState.Default, + val customServerDialogState: LoginSsoCustomServerDialogState? = null, +) { + val isLoading: Boolean + get() = flowState is LoginSsoFlowState.Loading + + val isSuccess: Boolean + get() = flowState is LoginSsoFlowState.Success + + val isError: Boolean + get() = flowState is LoginSsoFlowState.Error +} + +sealed interface LoginSsoFlowState { + data object Default : LoginSsoFlowState + data object Loading : LoginSsoFlowState + data object Canceled : LoginSsoFlowState + data class Success( + val initialSyncCompleted: Boolean, + val e2eiRequired: Boolean, + ) : LoginSsoFlowState + data class Error(val reason: LoginSsoError) : LoginSsoFlowState +} + +sealed interface LoginSsoError { + data object InvalidValue : LoginSsoError + data object InvalidSsoCode : LoginSsoError + data object InvalidSsoCookie : LoginSsoError + data object InvalidCredentials : LoginSsoError + data object ProxyError : LoginSsoError + data object UserAlreadyExists : LoginSsoError + data object PasswordNeededToRegisterClient : LoginSsoError + data object RequestTwoFactorAuthenticationWithHandle : LoginSsoError + data object ServerVersionNotSupported : LoginSsoError + data object ClientUpdateRequired : LoginSsoError + data object AccountSuspended : LoginSsoError + data object AccountPendingActivation : LoginSsoError + data object TooManyDevices : LoginSsoError + data class SsoResultError(val code: String) : LoginSsoError + data class GenericError(val message: String? = null) : LoginSsoError +} + +sealed interface LoginSsoIntent { + data class SsoCodeChanged(val ssoCode: String) : LoginSsoIntent + + data object SubmitLogin : LoginSsoIntent + + data object ClearLoginErrors : LoginSsoIntent + data object DismissCustomServerDialog : LoginSsoIntent + data object ConfirmCustomServerDialog : LoginSsoIntent + + data class AutoFillSsoCode( + val ssoCode: String, + val autoInitiateLogin: Boolean, + val nomadServiceUrl: String? = null, + val cookieLabel: String? = null, + ) : LoginSsoIntent + + data class CompleteSsoLogin( + val cookie: String, + val serverConfigId: String, + ) : LoginSsoIntent + + data class ReportSsoLoginFailure(val code: String) : LoginSsoIntent +} + +sealed interface LoginSsoEffect { + /** + * The platform layer should open [url] and route the callback back as an intent. + */ + data class OpenUrl( + val url: String, + val serverLinks: LoginServerLinks, + ) : LoginSsoEffect +} + +data class LoginSsoCustomServerDialogState( + val serverLinks: LoginServerLinks, +) diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoViewModelFactory.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoViewModelFactory.kt new file mode 100644 index 00000000000..291da7368a0 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoViewModelFactory.kt @@ -0,0 +1,173 @@ +/* + * 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.shared.auth.sso + +import com.wire.shared.auth.SharedViewModel +import com.wire.shared.auth.cancelScope +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +@Inject +class LoginSsoViewModelFactory( + private val backend: LoginSsoBackend, +) { + fun create( + coroutineContext: CoroutineContext = Dispatchers.Unconfined, + ): SharedViewModel { + val scope = CoroutineScope(SupervisorJob() + coroutineContext) + val state = MutableStateFlow(LoginSsoState()) + val effects = MutableSharedFlow(extraBufferCapacity = 1) + + return SharedViewModel( + state = state.asStateFlow(), + effects = effects.asSharedFlow(), + onIntent = { intent -> + when (intent) { + is LoginSsoIntent.SsoCodeChanged -> { + val ssoCode = intent.ssoCode + state.update { + it.copy( + ssoCode = ssoCode, + loginEnabled = ssoCode.isValidSsoCode(), + flowState = LoginSsoFlowState.Default, + ) + } + } + + LoginSsoIntent.SubmitLogin -> + submitLogin(state, effects, scope) + + LoginSsoIntent.ClearLoginErrors -> + state.update { it.copy(flowState = LoginSsoFlowState.Default) } + + LoginSsoIntent.DismissCustomServerDialog -> + state.update { it.copy(customServerDialogState = null) } + + LoginSsoIntent.ConfirmCustomServerDialog -> + state.update { it.copy(customServerDialogState = null) } + + is LoginSsoIntent.AutoFillSsoCode -> { + state.update { + it.copy( + ssoCode = intent.ssoCode, + loginEnabled = intent.ssoCode.isValidSsoCode(), + flowState = LoginSsoFlowState.Default, + ) + } + if (intent.autoInitiateLogin) { + submitLogin(state, effects, scope) + } + } + + is LoginSsoIntent.CompleteSsoLogin -> + completeLogin(intent, state, effects, scope) + + is LoginSsoIntent.ReportSsoLoginFailure -> + state.update { + it.copy(flowState = LoginSsoFlowState.Error(LoginSsoError.SsoResultError(intent.code))) + } + } + }, + onClose = { scope.cancelScope() }, + ) + } + + private fun submitLogin( + state: MutableStateFlow, + effects: MutableSharedFlow, + scope: CoroutineScope, + ) { + val ssoCode = state.value.ssoCode.trim() + if (!ssoCode.isValidSsoCode()) { + state.update { it.copy(flowState = LoginSsoFlowState.Error(LoginSsoError.InvalidSsoCode)) } + return + } + + state.update { it.copy(flowState = LoginSsoFlowState.Loading) } + scope.launch { + when (val result = backend.initiateLogin(ssoCode)) { + is LoginSsoBackendResult.Error -> + state.update { it.copy(flowState = LoginSsoFlowState.Error(result.reason)) } + + is LoginSsoBackendResult.OpenUrl -> { + effects.tryEmit( + LoginSsoEffect.OpenUrl( + url = result.url, + serverLinks = result.serverLinks, + ) + ) + state.update { it.copy(flowState = LoginSsoFlowState.Default) } + } + + is LoginSsoBackendResult.Success -> + state.update { it.withSuccess(result) } + } + } + } + + private fun completeLogin( + intent: LoginSsoIntent.CompleteSsoLogin, + state: MutableStateFlow, + effects: MutableSharedFlow, + scope: CoroutineScope, + ) { + state.update { it.copy(flowState = LoginSsoFlowState.Loading) } + scope.launch { + when (val result = backend.completeLogin(intent.cookie, intent.serverConfigId)) { + is LoginSsoBackendResult.Error -> + state.update { it.copy(flowState = LoginSsoFlowState.Error(result.reason)) } + + is LoginSsoBackendResult.OpenUrl -> { + effects.tryEmit( + LoginSsoEffect.OpenUrl( + url = result.url, + serverLinks = result.serverLinks, + ) + ) + state.update { it.copy(flowState = LoginSsoFlowState.Default) } + } + + is LoginSsoBackendResult.Success -> + state.update { it.withSuccess(result) } + } + } + } +} + +private fun LoginSsoState.withSuccess(result: LoginSsoBackendResult.Success): LoginSsoState = + copy( + flowState = LoginSsoFlowState.Success( + initialSyncCompleted = result.initialSyncCompleted, + e2eiRequired = result.e2eiRequired, + ) + ) + +private fun String.isValidSsoCode(): Boolean = + startsWith(SSO_CODE_PREFIX) && removePrefix(SSO_CODE_PREFIX).matches(uuidRegex) + +private const val SSO_CODE_PREFIX = "wire-" +private val uuidRegex = Regex("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeEffect.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeEffect.kt new file mode 100644 index 00000000000..487380db78f --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeEffect.kt @@ -0,0 +1,33 @@ +/* + * 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.shared.auth.welcome + +import com.wire.shared.auth.login.model.LoginServerLinks + +sealed interface WelcomeEffect { + data class NavigateToLogin(val links: LoginServerLinks) : WelcomeEffect + data class NavigateToCreatePersonalAccount(val links: LoginServerLinks) : WelcomeEffect + data class NavigateToCreateTeamAccount(val links: LoginServerLinks) : WelcomeEffect + data class OpenExternalUrl(val url: String) : WelcomeEffect + data class ShowProxyLimitation(val target: WelcomeProxyLimitedTarget) : WelcomeEffect +} + +enum class WelcomeProxyLimitedTarget { + PersonalAccountCreation, + TeamAccountCreation, +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeIntent.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeIntent.kt new file mode 100644 index 00000000000..7aa5b871135 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeIntent.kt @@ -0,0 +1,25 @@ +/* + * 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.shared.auth.welcome + +sealed interface WelcomeIntent { + data object LoginClicked : WelcomeIntent + data object CreatePersonalAccountClicked : WelcomeIntent + data object CreateTeamAccountClicked : WelcomeIntent + data object ProxyLimitationDismissed : WelcomeIntent +} diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeState.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeState.kt new file mode 100644 index 00000000000..02d7a83d5cc --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeState.kt @@ -0,0 +1,29 @@ +/* + * 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.shared.auth.welcome + +import com.wire.shared.auth.login.model.LoginServerLinks + +data class WelcomeState( + val links: LoginServerLinks, + val isThereActiveSession: Boolean = false, + val maxAccountsReached: Boolean = false, + val nomadAccountBlocksLogin: Boolean = false, + val isAccountCreationAllowed: Boolean = true, + val useNewRegistration: Boolean = true, +) diff --git a/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactory.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactory.kt new file mode 100644 index 00000000000..c4bfae66a7a --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactory.kt @@ -0,0 +1,89 @@ +/* + * 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.shared.auth.welcome + +import com.wire.shared.auth.SharedAuthConfig +import com.wire.shared.auth.SharedViewModel +import com.wire.shared.auth.login.model.LoginServerLinks +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow + +@Inject +class WelcomeViewModelFactory( + private val config: SharedAuthConfig, +) { + fun create(): SharedViewModel { + val state = MutableStateFlow( + WelcomeState( + links = config.defaultServerLinks, + isThereActiveSession = config.isThereActiveSession, + maxAccountsReached = config.maxAccountsReached, + nomadAccountBlocksLogin = config.nomadAccountBlocksLogin, + isAccountCreationAllowed = config.isAccountCreationAllowed, + useNewRegistration = config.useNewRegistration, + ) + ) + val effects = MutableSharedFlow(extraBufferCapacity = 1) + + return SharedViewModel( + state = state.asStateFlow(), + effects = effects.asSharedFlow(), + onIntent = { intent -> + when (intent) { + WelcomeIntent.LoginClicked -> + effects.tryEmit(WelcomeEffect.NavigateToLogin(state.value.links)) + + WelcomeIntent.CreatePersonalAccountClicked -> + effects.tryEmit( + createAccountEffect( + state = state.value, + proxyLimitedTarget = WelcomeProxyLimitedTarget.PersonalAccountCreation, + navigate = WelcomeEffect::NavigateToCreatePersonalAccount, + ) + ) + + WelcomeIntent.CreateTeamAccountClicked -> + effects.tryEmit( + createAccountEffect( + state = state.value, + proxyLimitedTarget = WelcomeProxyLimitedTarget.TeamAccountCreation, + navigate = WelcomeEffect::NavigateToCreateTeamAccount, + ) + ) + + WelcomeIntent.ProxyLimitationDismissed -> + Unit + } + } + ) + } + + private fun createAccountEffect( + state: WelcomeState, + proxyLimitedTarget: WelcomeProxyLimitedTarget, + navigate: (links: LoginServerLinks) -> WelcomeEffect, + ): WelcomeEffect = + if (state.links.isProxyEnabled) { + WelcomeEffect.ShowProxyLimitation(proxyLimitedTarget) + } else { + navigate(state.links) + } +} diff --git a/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/email/LoginEmailContractTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/email/LoginEmailContractTest.kt new file mode 100644 index 00000000000..7ca02d3a7f9 --- /dev/null +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/email/LoginEmailContractTest.kt @@ -0,0 +1,94 @@ +/* + * 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.shared.auth.email + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class LoginEmailContractTest { + + @Test + fun givenDefaultState_whenCreated_thenMatchesLoginEmailDefaults() { + val state = LoginEmailState() + + assertEquals("", state.userIdentifier) + assertEquals("", state.password) + assertTrue(state.isPasswordEmpty) + assertFalse(state.isPasswordNotEmpty) + assertEquals("", state.proxyIdentifier) + assertEquals("", state.proxyPassword) + assertTrue(state.userIdentifierEnabled) + assertFalse(state.loginEnabled) + assertFalse(state.isLoading) + assertFalse(state.isSuccess) + assertFalse(state.isInvalidCredentials) + assertIs(state.flowState) + assertEquals(LoginEmailVerificationCodeState.DEFAULT_VERIFICATION_CODE_LENGTH, state.secondFactorVerificationCode.codeLength) + assertFalse(state.secondFactorVerificationCode.isCodeInputNecessary) + assertFalse(state.secondFactorVerificationCode.isCurrentCodeInvalid) + assertFalse(state.isSecondFactorRequired) + assertFalse(state.isSecondFactorInvalid) + assertEquals("", state.secondFactorCode) + assertFalse(state.isSecondFactorCodeComplete) + assertEquals("", state.secondFactorEmail) + } + + @Test + fun givenPasswordAndSecondFactorState_whenCreated_thenExposesPlatformFriendlyUiFlags() { + val state = LoginEmailState( + password = "password", + flowState = LoginEmailFlowState.Error(LoginEmailError.InvalidCredentials), + secondFactorVerificationCode = LoginEmailVerificationCodeState( + code = "123456", + emailUsed = "user@example.com", + isCodeInputNecessary = true, + isCurrentCodeInvalid = true, + ), + ) + + assertFalse(state.isPasswordEmpty) + assertTrue(state.isPasswordNotEmpty) + assertTrue(state.isInvalidCredentials) + assertTrue(state.isSecondFactorRequired) + assertTrue(state.isSecondFactorInvalid) + assertEquals("123456", state.secondFactorCode) + assertTrue(state.isSecondFactorCodeComplete) + assertEquals("user@example.com", state.secondFactorEmail) + } + + @Test + fun givenSubmitLoginIntent_whenCreatedWithoutOverride_thenUsernameIsAllowed() { + val intent = LoginEmailIntent.SubmitLogin() + + assertTrue(intent.usernameAllowed) + } + + @Test + fun givenSuccessEffect_whenCreated_thenCarriesNavigationFlags() { + val effect = LoginEmailEffect.LoginSucceeded( + initialSyncCompleted = true, + isE2EIRequired = false, + ) + + assertTrue(effect.initialSyncCompleted) + assertFalse(effect.isE2EIRequired) + } +} diff --git a/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/email/LoginEmailViewModelFactoryTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/email/LoginEmailViewModelFactoryTest.kt new file mode 100644 index 00000000000..66d70ff4ad3 --- /dev/null +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/email/LoginEmailViewModelFactoryTest.kt @@ -0,0 +1,279 @@ +/* + * 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.shared.auth.email + +import com.wire.shared.auth.AuthLoginSuccessPayload +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class LoginEmailViewModelFactoryTest { + @Test + fun givenCredentials_whenSubmitting_thenLoginSucceededEffectIsEmitted() = runTest { + val viewModel = LoginEmailViewModelFactory(SuccessGateway).create() + viewModel.sendIntent(LoginEmailIntent.UserIdentifierChanged("user@example.com")) + viewModel.sendIntent(LoginEmailIntent.PasswordChanged("password")) + val effect = async(start = CoroutineStart.UNDISPATCHED) { + viewModel.effects.first() + } + + viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) + runCurrent() + + assertIs(effect.await()) + assertEquals(LoginEmailFlowState.Success(initialSyncCompleted = false, isE2EIRequired = false), viewModel.currentState.flowState) + } + + @Test + fun givenSecondFactorRequired_whenSubmitting_thenVerificationCodeStateIsShown() = runTest { + val viewModel = LoginEmailViewModelFactory(SecondFactorGateway).create() + viewModel.sendIntent(LoginEmailIntent.UserIdentifierChanged("user@example.com")) + viewModel.sendIntent(LoginEmailIntent.PasswordChanged("password")) + + viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) + runCurrent() + + assertEquals(true, viewModel.currentState.secondFactorVerificationCode.isCodeInputNecessary) + assertEquals("user@example.com", viewModel.currentState.secondFactorVerificationCode.emailUsed) + } + + @Test + fun givenSecondFactorRequired_whenSubmittingCode_thenLoginSucceededEffectIsEmitted() = runTest { + SecondFactorThenSuccessGateway.receivedSecondFactorVerificationCode = null + val viewModel = LoginEmailViewModelFactory(SecondFactorThenSuccessGateway).create() + viewModel.sendIntent(LoginEmailIntent.UserIdentifierChanged("user@example.com")) + viewModel.sendIntent(LoginEmailIntent.PasswordChanged("password")) + val effect = async(start = CoroutineStart.UNDISPATCHED) { + viewModel.effects.first() + } + + viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) + runCurrent() + + assertTrue(viewModel.currentState.isSecondFactorRequired) + assertEquals("user@example.com", viewModel.currentState.secondFactorEmail) + + viewModel.sendIntent(LoginEmailIntent.SecondFactorCodeChanged("123456")) + + assertEquals("123456", viewModel.currentState.secondFactorCode) + assertTrue(viewModel.currentState.isSecondFactorCodeComplete) + + viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) + runCurrent() + + assertIs(effect.await()) + assertEquals(LoginEmailFlowState.Success(initialSyncCompleted = true, isE2EIRequired = false), viewModel.currentState.flowState) + assertTrue(viewModel.currentState.isSuccess) + assertEquals("123456", SecondFactorThenSuccessGateway.receivedSecondFactorVerificationCode) + } + + @Test + fun givenInvalidSecondFactorCode_whenSubmitting_thenInvalidSecondFactorStateIsShownWithoutEffect() = runTest { + val viewModel = LoginEmailViewModelFactory(InvalidSecondFactorGateway).create() + val effects = mutableListOf() + val effectCollection = backgroundScope.launch { + viewModel.effects.toList(effects) + } + viewModel.sendIntent(LoginEmailIntent.UserIdentifierChanged("user@example.com")) + viewModel.sendIntent(LoginEmailIntent.PasswordChanged("password")) + + viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) + runCurrent() + viewModel.sendIntent(LoginEmailIntent.SecondFactorCodeChanged("000000")) + viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) + runCurrent() + + assertTrue(viewModel.currentState.isSecondFactorRequired) + assertTrue(viewModel.currentState.isSecondFactorInvalid) + assertEquals("000000", viewModel.currentState.secondFactorCode) + assertEquals("user@example.com", viewModel.currentState.secondFactorEmail) + assertEquals(LoginEmailFlowState.Default, viewModel.currentState.flowState) + assertTrue(effects.isEmpty()) + effectCollection.cancel() + } + + @Test + fun givenInvalidSecondFactorCode_whenCodeChanges_thenInvalidSecondFactorStateIsCleared() = runTest { + val viewModel = LoginEmailViewModelFactory(InvalidSecondFactorGateway).create() + viewModel.sendIntent(LoginEmailIntent.UserIdentifierChanged("user@example.com")) + viewModel.sendIntent(LoginEmailIntent.PasswordChanged("password")) + + viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) + runCurrent() + viewModel.sendIntent(LoginEmailIntent.SecondFactorCodeChanged("000000")) + viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) + runCurrent() + + assertTrue(viewModel.currentState.isSecondFactorInvalid) + + viewModel.sendIntent(LoginEmailIntent.SecondFactorCodeChanged("123456")) + + assertEquals("123456", viewModel.currentState.secondFactorCode) + assertTrue(viewModel.currentState.isSecondFactorCodeComplete) + assertEquals(false, viewModel.currentState.isSecondFactorInvalid) + } + + @Test + fun givenInvalidCredentials_whenSubmitting_thenErrorStateIsShownWithoutEffect() = runTest { + val viewModel = LoginEmailViewModelFactory(InvalidCredentialsGateway).create() + val effects = mutableListOf() + val effectCollection = backgroundScope.launch { + viewModel.effects.toList(effects) + } + viewModel.sendIntent(LoginEmailIntent.UserIdentifierChanged("user@example.com")) + viewModel.sendIntent(LoginEmailIntent.PasswordChanged("wrong-password")) + + viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) + runCurrent() + + assertEquals(LoginEmailFlowState.Error(LoginEmailError.InvalidCredentials), viewModel.currentState.flowState) + assertEquals(true, viewModel.currentState.loginEnabled) + assertTrue(effects.isEmpty()) + effectCollection.cancel() + } + + private object SuccessGateway : LoginEmailGateway { + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailGatewayResult = + LoginEmailGatewayResult.Success( + initialSyncCompleted = false, + isE2EIRequired = false, + payload = successPayload( + userIdentifier = userIdentifier, + password = password, + secondFactorVerificationCode = secondFactorVerificationCode, + initialSyncCompleted = false, + isE2EIRequired = false, + ), + ) + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailGatewayResult = + LoginEmailGatewayResult.SecondFactorRequired(userIdentifier) + } + + private object SecondFactorGateway : LoginEmailGateway { + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailGatewayResult = + LoginEmailGatewayResult.SecondFactorRequired(userIdentifier) + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailGatewayResult = + LoginEmailGatewayResult.SecondFactorRequired(userIdentifier) + } + + private object SecondFactorThenSuccessGateway : LoginEmailGateway { + var receivedSecondFactorVerificationCode: String? = null + + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailGatewayResult = + if (secondFactorVerificationCode == null) { + LoginEmailGatewayResult.SecondFactorRequired(userIdentifier) + } else { + receivedSecondFactorVerificationCode = secondFactorVerificationCode + LoginEmailGatewayResult.Success( + initialSyncCompleted = true, + isE2EIRequired = false, + payload = successPayload( + userIdentifier = userIdentifier, + password = password, + secondFactorVerificationCode = secondFactorVerificationCode, + initialSyncCompleted = true, + isE2EIRequired = false, + ), + ) + } + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailGatewayResult = + LoginEmailGatewayResult.SecondFactorRequired(userIdentifier) + } + + private object InvalidSecondFactorGateway : LoginEmailGateway { + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailGatewayResult = + LoginEmailGatewayResult.SecondFactorRequired( + email = userIdentifier, + isCurrentCodeInvalid = secondFactorVerificationCode != null, + ) + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailGatewayResult = + LoginEmailGatewayResult.SecondFactorRequired(userIdentifier) + } + + private object InvalidCredentialsGateway : LoginEmailGateway { + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailGatewayResult = + LoginEmailGatewayResult.Failure(LoginEmailError.InvalidCredentials) + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailGatewayResult = + LoginEmailGatewayResult.SecondFactorRequired(userIdentifier) + } + + private companion object { + fun successPayload( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + initialSyncCompleted: Boolean, + isE2EIRequired: Boolean, + ): AuthLoginSuccessPayload = + AuthLoginSuccessPayload( + userIdValue = "user-id", + userIdDomain = "wire.com", + accessTokenValue = "access-token", + accessTokenType = "Bearer", + accessTokenExpiresInSeconds = null, + refreshTokenValue = "refresh-token", + refreshTokenCookieDomain = "wire.com", + email = userIdentifier, + password = password, + secondFactorCode = secondFactorVerificationCode, + initialSyncCompleted = initialSyncCompleted, + isE2EIRequired = isE2EIRequired, + clientId = "client-id", + ) + } +} diff --git a/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/flow/AuthLoginFlowViewModelFactoryTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/flow/AuthLoginFlowViewModelFactoryTest.kt new file mode 100644 index 00000000000..93f0925d9f9 --- /dev/null +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/flow/AuthLoginFlowViewModelFactoryTest.kt @@ -0,0 +1,164 @@ +/* + * 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.shared.auth.flow + +import com.wire.shared.auth.AuthLoginSuccessPayload +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class AuthLoginFlowViewModelFactoryTest { + @Test + fun givenEmailFlow_whenPasswordAndSecondFactorAreAccepted_thenSuccessIsShown() = runTest { + val backend = TwoFactorBackend() + val viewModel = AuthLoginFlowViewModelFactory(backend).create() + + viewModel.sendIntent(AuthLoginFlowIntent.IdentifierChanged("user@example.com")) + viewModel.sendIntent(AuthLoginFlowIntent.SubmitIdentifier) + assertEquals(AuthLoginFlowStep.EmailCredentialsEntry, viewModel.currentState.step) + + viewModel.sendIntent(AuthLoginFlowIntent.PasswordChanged("password")) + viewModel.sendIntent(AuthLoginFlowIntent.SubmitCredentials()) + assertEquals(AuthLoginFlowStep.SecondFactorEntry, viewModel.currentState.step) + assertEquals("user@example.com", viewModel.currentState.secondFactorEmail) + + viewModel.sendIntent(AuthLoginFlowIntent.SecondFactorCodeChanged("123456")) + val successEffect = async(start = CoroutineStart.UNDISPATCHED) { + viewModel.effects.first() + } + viewModel.sendIntent(AuthLoginFlowIntent.SubmitSecondFactor()) + + val success = assertIs(viewModel.currentState.step) + assertEquals(false, success.initialSyncCompleted) + assertEquals(false, success.isE2EIRequired) + val loginSucceeded = assertIs(successEffect.await()) + assertEquals("user@example.com", loginSucceeded.payload.email) + assertEquals("password", loginSucceeded.payload.password) + assertEquals("123456", loginSucceeded.payload.secondFactorCode) + assertTrue(viewModel.currentState.isSuccess) + assertEquals( + listOf( + LoginCall( + identifier = "user@example.com", + password = "password", + secondFactorCode = null, + ), + LoginCall( + identifier = "user@example.com", + password = "password", + secondFactorCode = "123456", + ), + ), + backend.loginCalls, + ) + } + + @Test + fun givenSsoCode_whenSubmitting_thenBackendIsDispatchedAndOpenSsoEffectIsEmitted() = runTest { + val backend = SsoBackend() + val viewModel = AuthLoginFlowViewModelFactory(backend).create() + val effect = async(start = CoroutineStart.UNDISPATCHED) { + viewModel.effects.first() + } + + viewModel.sendIntent(AuthLoginFlowIntent.SsoCodeChanged("wire-123")) + viewModel.sendIntent(AuthLoginFlowIntent.SubmitSsoCode) + + val openSso = assertIs(effect.await()) + assertEquals("https://sso.example.com/login", openSso.url) + assertEquals("wire-123", openSso.userIdentifier) + assertEquals(listOf("wire-123"), backend.ssoCodes) + assertEquals(AuthLoginFlowStep.IdentifierEntry, viewModel.currentState.step) + } + + private class TwoFactorBackend : AuthLoginFlowBackend { + val loginCalls = mutableListOf() + + override suspend fun resolveIdentifier(identifier: String): AuthLoginFlowIdentifierResult = + AuthLoginFlowIdentifierResult.EmailCredentialsRequired(identifier) + + override suspend fun initiateSso(ssoCode: String): AuthLoginFlowIdentifierResult = + error("SSO should not be used by the email path") + + override suspend fun loginWithEmail( + identifier: String, + password: String, + secondFactorCode: String?, + usernameAllowed: Boolean, + ): AuthLoginFlowLoginResult { + loginCalls += LoginCall(identifier, password, secondFactorCode) + return if (secondFactorCode == null) { + AuthLoginFlowLoginResult.SecondFactorRequired(email = identifier) + } else { + AuthLoginFlowLoginResult.Success( + initialSyncCompleted = false, + isE2EIRequired = false, + payload = AuthLoginSuccessPayload( + userIdValue = "user-id", + userIdDomain = "wire.com", + accessTokenValue = "access-token", + accessTokenType = "Bearer", + accessTokenExpiresInSeconds = null, + refreshTokenValue = "refresh-token", + refreshTokenCookieDomain = "wire.com", + email = identifier, + password = password, + secondFactorCode = secondFactorCode, + initialSyncCompleted = false, + isE2EIRequired = false, + clientId = "client-id", + ), + ) + } + } + } + + private class SsoBackend : AuthLoginFlowBackend { + val ssoCodes = mutableListOf() + + override suspend fun resolveIdentifier(identifier: String): AuthLoginFlowIdentifierResult = + error("Email resolution should not be used by the SSO code path") + + override suspend fun initiateSso(ssoCode: String): AuthLoginFlowIdentifierResult { + ssoCodes += ssoCode + return AuthLoginFlowIdentifierResult.OpenSso( + url = "https://sso.example.com/login", + userIdentifier = ssoCode, + ) + } + + override suspend fun loginWithEmail( + identifier: String, + password: String, + secondFactorCode: String?, + usernameAllowed: Boolean, + ): AuthLoginFlowLoginResult = + error("Email login should not be used by the SSO code path") + } + + private data class LoginCall( + val identifier: String, + val password: String, + val secondFactorCode: String?, + ) +} diff --git a/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/login/model/LoginNavArgsTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/login/model/LoginNavArgsTest.kt new file mode 100644 index 00000000000..aa4db7d2eaf --- /dev/null +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/login/model/LoginNavArgsTest.kt @@ -0,0 +1,43 @@ +/* + * 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.shared.auth.login.model + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class LoginNavArgsTest { + @Test + fun givenNoUserIdentifier_whenCheckingEditable_thenItIsEditable() { + assertTrue(LoginUserIdentifier.None.userIdentifierEditable) + } + + @Test + fun givenPreFilledUserIdentifierWithDefaultEditable_whenCheckingEditable_thenItIsNotEditable() { + val userIdentifier = LoginUserIdentifier.PreFilled(value = "user@example.com") + + assertFalse(userIdentifier.userIdentifierEditable) + } + + @Test + fun givenPreFilledUserIdentifierWithEditableTrue_whenCheckingEditable_thenItIsEditable() { + val userIdentifier = LoginUserIdentifier.PreFilled(value = "user@example.com", editable = true) + + assertTrue(userIdentifier.userIdentifierEditable) + } +} diff --git a/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt new file mode 100644 index 00000000000..f347e30b952 --- /dev/null +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt @@ -0,0 +1,88 @@ +/* + * 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.shared.auth.newlogin + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class NewLoginIdentifierStateReducerTest { + + @Test + fun givenDefaultState_whenUserIdentifierIsEntered_thenNextIsEnabled() { + val state = NewLoginIdentifierState().withUserIdentifier("user@example.com") + + assertEquals("user@example.com", state.userIdentifier) + assertTrue(state.nextEnabled) + assertEquals(NewLoginIdentifierFlowState.Default, state.flowState) + assertFalse(state.isLoading) + assertFalse(state.hasTextFieldError) + assertNull(state.textFieldError) + assertFalse(state.hasDialogError) + assertNull(state.dialogError) + assertFalse(state.isCustomConfigDialogVisible) + assertNull(state.customConfigServerLinks) + } + + @Test + fun givenTextFieldError_whenUserIdentifierChanges_thenErrorIsCleared() { + val state = NewLoginIdentifierState( + flowState = NewLoginIdentifierFlowState.TextFieldError(NewLoginIdentifierTextFieldError.InvalidValue), + ).withUserIdentifier("user@example.com") + + assertEquals(NewLoginIdentifierFlowState.Default, state.flowState) + assertFalse(state.hasTextFieldError) + assertNull(state.textFieldError) + } + + @Test + fun givenUserIdentifier_whenLoadingStarts_thenNextIsDisabled() { + val state = NewLoginIdentifierState(userIdentifier = "user@example.com") + .withFlowState(NewLoginIdentifierFlowState.Loading) + + assertFalse(state.nextEnabled) + assertTrue(state.isLoading) + } + + @Test + fun givenTextFieldErrorState_whenReadByPlatform_thenConvenienceFieldsAreExposed() { + val state = NewLoginIdentifierState( + flowState = NewLoginIdentifierFlowState.TextFieldError(NewLoginIdentifierTextFieldError.InvalidValue), + ) + + assertTrue(state.hasTextFieldError) + assertEquals(NewLoginIdentifierTextFieldError.InvalidValue, state.textFieldError) + assertFalse(state.hasDialogError) + assertNull(state.dialogError) + } + + @Test + fun givenDialogErrorState_whenReadByPlatform_thenConvenienceFieldsAreExposed() { + val error = NewLoginIdentifierDialogError.SSOResultFailure(NewLoginSsoFailureCode.InvalidCode) + val state = NewLoginIdentifierState( + flowState = NewLoginIdentifierFlowState.DialogError(error), + ) + + assertTrue(state.hasDialogError) + assertEquals(error, state.dialogError) + assertFalse(state.hasTextFieldError) + assertNull(state.textFieldError) + } +} diff --git a/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierViewModelFactoryTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierViewModelFactoryTest.kt new file mode 100644 index 00000000000..04c80bd81c2 --- /dev/null +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierViewModelFactoryTest.kt @@ -0,0 +1,169 @@ +/* + * 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.shared.auth.newlogin + +import com.wire.shared.auth.SharedAuthConfig +import com.wire.shared.auth.login.model.LoginServerLinks +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertFalse + +class NewLoginIdentifierViewModelFactoryTest { + @Test + fun givenIdentifierChanged_whenSendingIntent_thenStateIsUpdated() { + val viewModel = newViewModel() + + viewModel.sendIntent(NewLoginIdentifierIntent.UserIdentifierChanged("user@example.com")) + + assertEquals("user@example.com", viewModel.currentState.userIdentifier) + assertEquals(true, viewModel.currentState.nextEnabled) + } + + @Test + fun givenInvalidIdentifier_whenSubmitting_thenTextFieldErrorIsShown() { + val viewModel = newViewModel() + viewModel.sendIntent(NewLoginIdentifierIntent.UserIdentifierChanged("invalid")) + + viewModel.sendIntent(NewLoginIdentifierIntent.Submit) + + assertEquals( + NewLoginIdentifierFlowState.TextFieldError(NewLoginIdentifierTextFieldError.InvalidValue), + viewModel.currentState.flowState, + ) + } + + @Test + fun givenEmailIdentifier_whenSubmitting_thenOpenEmailPasswordEffectIsEmitted() = runTest { + val viewModel = newViewModel() + viewModel.sendIntent(NewLoginIdentifierIntent.UserIdentifierChanged("user@example.com")) + val effect = async(start = CoroutineStart.UNDISPATCHED) { + viewModel.effects.first() + } + + viewModel.sendIntent(NewLoginIdentifierIntent.Submit) + + val openEmailPassword = assertIs(effect.await()) + assertEquals("user@example.com", openEmailPassword.userIdentifier) + assertEquals(NewLoginIdentifierFlowState.Default, viewModel.currentState.flowState) + assertFalse(viewModel.currentState.isLoading) + assertFalse(viewModel.currentState.hasTextFieldError) + assertFalse(viewModel.currentState.hasDialogError) + } + + @Test + fun givenSsoCodeIdentifier_whenSubmitting_thenOpenSsoEffectIsEmitted() = runTest { + val ssoCode = "wire-123e4567-e89b-12d3-a456-426614174000" + val expectedUrl = "https://sso.example.com/login" + val viewModel = newViewModel( + backend = FakeNewLoginIdentifierBackend( + ssoResult = NewLoginIdentifierBackendResult.OpenSso( + url = expectedUrl, + config = NewLoginSsoUrlConfig(userIdentifier = ssoCode), + ) + ) + ) + viewModel.sendIntent(NewLoginIdentifierIntent.UserIdentifierChanged(ssoCode)) + val effect = async(start = CoroutineStart.UNDISPATCHED) { + viewModel.effects.first() + } + + viewModel.sendIntent(NewLoginIdentifierIntent.Submit) + + val openSso = assertIs(effect.await()) + assertEquals(expectedUrl, openSso.url) + assertEquals(ssoCode, openSso.config.userIdentifier) + assertEquals(NewLoginIdentifierFlowState.Default, viewModel.currentState.flowState) + } + + @Test + fun givenSsoFailure_whenReceived_thenDialogErrorIsShown() { + val viewModel = newViewModel() + + viewModel.sendIntent(NewLoginIdentifierIntent.SSOResultReceived(NewLoginSsoResult.Failure(NewLoginSsoFailureCode.InvalidCode))) + + assertEquals( + NewLoginIdentifierFlowState.DialogError( + NewLoginIdentifierDialogError.SSOResultFailure(NewLoginSsoFailureCode.InvalidCode) + ), + viewModel.currentState.flowState, + ) + assertEquals(NewLoginIdentifierDialogError.SSOResultFailure(NewLoginSsoFailureCode.InvalidCode), viewModel.currentState.dialogError) + } + + @Test + fun givenSsoSuccess_whenReceived_thenLoginSucceededEffectIsEmitted() = runTest { + val viewModel = newViewModel() + val effect = async(start = CoroutineStart.UNDISPATCHED) { + viewModel.effects.first() + } + + viewModel.sendIntent( + NewLoginIdentifierIntent.SSOResultReceived( + NewLoginSsoResult.Success( + cookie = "cookie", + serverConfigId = "server-config-id", + ) + ) + ) + + val loginSucceeded = assertIs(effect.await()) + assertEquals(NewLoginSuccessNextStep.None, loginSucceeded.nextStep) + assertEquals(NewLoginIdentifierFlowState.Default, viewModel.currentState.flowState) + } + + private companion object { + fun newViewModel( + backend: NewLoginIdentifierBackend = LocalNewLoginIdentifierBackend(), + ): com.wire.shared.auth.SharedViewModel = + NewLoginIdentifierViewModelFactory( + config = SharedAuthConfig(serverLinks), + backend = backend, + ).create() + + val serverLinks = LoginServerLinks( + api = "https://api.example.com", + accounts = "https://accounts.example.com", + webSocket = "wss://websocket.example.com", + blackList = "https://blacklist.example.com", + teams = "https://teams.example.com", + website = "https://www.example.com", + title = "Example", + isOnPremises = false, + ) + } + + private class FakeNewLoginIdentifierBackend( + private val emailResult: NewLoginIdentifierBackendResult = NewLoginIdentifierBackendResult.OpenEmailPassword( + userIdentifier = "user@example.com", + path = NewLoginPasswordPath(), + ), + private val ssoResult: NewLoginIdentifierBackendResult = NewLoginIdentifierBackendResult.OpenSso( + url = "https://sso.example.com/login", + config = NewLoginSsoUrlConfig(userIdentifier = "wire-123e4567-e89b-12d3-a456-426614174000"), + ), + ) : NewLoginIdentifierBackend { + override suspend fun resolveEmail(userIdentifier: String): NewLoginIdentifierBackendResult = emailResult + + override suspend fun initiateSso(ssoCode: String): NewLoginIdentifierBackendResult = ssoResult + } +} diff --git a/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/sso/LoginSsoContractTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/sso/LoginSsoContractTest.kt new file mode 100644 index 00000000000..43eaa509683 --- /dev/null +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/sso/LoginSsoContractTest.kt @@ -0,0 +1,79 @@ +/* + * 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.shared.auth.sso + +import com.wire.shared.auth.login.model.LoginServerLinks +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull + +class LoginSsoContractTest { + + @Test + fun givenNoInput_whenStateIsCreated_thenItUsesIdleDefaults() { + val state = LoginSsoState() + + assertEquals("", state.ssoCode) + assertFalse(state.loginEnabled) + assertEquals(LoginSsoFlowState.Default, state.flowState) + assertNull(state.customServerDialogState) + } + + @Test + fun givenOpenUrlEffect_whenCreated_thenItCarriesOnlyPlatformFacingData() { + val effect = LoginSsoEffect.OpenUrl( + url = "wire://sso/start", + serverLinks = serverLinks, + ) + + assertEquals("wire://sso/start", effect.url) + assertEquals(serverLinks, effect.serverLinks) + } + + @Test + fun givenSsoResultFailure_whenReported_thenItKeepsPlatformErrorCode() { + val intent = LoginSsoIntent.ReportSsoLoginFailure("access-denied") + + assertEquals("access-denied", intent.code) + } + + @Test + fun givenLoginError_whenStoredInFlowState_thenItCanBePatternMatched() { + val state = LoginSsoState( + flowState = LoginSsoFlowState.Error(LoginSsoError.InvalidSsoCode), + ) + + val errorState = assertIs(state.flowState) + assertEquals(LoginSsoError.InvalidSsoCode, errorState.reason) + } + + private companion object { + val serverLinks = LoginServerLinks( + api = "https://api.example.com", + accounts = "https://accounts.example.com", + webSocket = "wss://websocket.example.com", + blackList = "https://blacklist.example.com", + teams = "https://teams.example.com", + website = "https://example.com", + title = "Example", + isOnPremises = false, + ) + } +} diff --git a/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/sso/LoginSsoViewModelFactoryTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/sso/LoginSsoViewModelFactoryTest.kt new file mode 100644 index 00000000000..fcd1b67f238 --- /dev/null +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/sso/LoginSsoViewModelFactoryTest.kt @@ -0,0 +1,131 @@ +/* + * 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.shared.auth.sso + +import com.wire.shared.auth.login.model.LoginServerLinks +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class LoginSsoViewModelFactoryTest { + @Test + fun givenSsoCodeChanged_whenSendingIntent_thenStateIsUpdated() { + val viewModel = newViewModel(OpenUrlBackend) + + viewModel.sendIntent(LoginSsoIntent.SsoCodeChanged(validSsoCode)) + + assertEquals(validSsoCode, viewModel.currentState.ssoCode) + assertEquals(true, viewModel.currentState.loginEnabled) + assertEquals(LoginSsoFlowState.Default, viewModel.currentState.flowState) + } + + @Test + fun givenInvalidSsoCode_whenSubmitting_thenErrorStateIsShown() { + val viewModel = newViewModel(OpenUrlBackend) + viewModel.sendIntent(LoginSsoIntent.SsoCodeChanged("invalid")) + + viewModel.sendIntent(LoginSsoIntent.SubmitLogin) + + assertEquals(LoginSsoFlowState.Error(LoginSsoError.InvalidSsoCode), viewModel.currentState.flowState) + } + + @Test + fun givenValidSsoCode_whenSubmitting_thenOpenUrlEffectIsEmitted() = runTest { + val viewModel = newViewModel(OpenUrlBackend) + viewModel.sendIntent(LoginSsoIntent.SsoCodeChanged(validSsoCode)) + val effect = async(start = CoroutineStart.UNDISPATCHED) { + viewModel.effects.first() + } + + viewModel.sendIntent(LoginSsoIntent.SubmitLogin) + + val openUrl = assertIs(effect.await()) + assertEquals("https://accounts.example.com/sso", openUrl.url) + assertEquals(serverLinks, openUrl.serverLinks) + assertEquals(LoginSsoFlowState.Default, viewModel.currentState.flowState) + } + + @Test + fun givenSsoCallback_whenCompletingLogin_thenSuccessStateIsShown() { + val viewModel = newViewModel(SuccessBackend) + + viewModel.sendIntent(LoginSsoIntent.CompleteSsoLogin(cookie = "cookie", serverConfigId = "server")) + + assertEquals( + LoginSsoFlowState.Success(initialSyncCompleted = false, e2eiRequired = false), + viewModel.currentState.flowState, + ) + } + + @Test + fun givenSsoFailureCallback_whenReported_thenErrorStateKeepsCode() { + val viewModel = newViewModel(SuccessBackend) + + viewModel.sendIntent(LoginSsoIntent.ReportSsoLoginFailure("access-denied")) + + val error = assertIs(viewModel.currentState.flowState) + assertEquals(LoginSsoError.SsoResultError("access-denied"), error.reason) + } + + private companion object { + fun newViewModel(backend: LoginSsoBackend): com.wire.shared.auth.SharedViewModel = + LoginSsoViewModelFactory(backend).create() + + const val validSsoCode = "wire-123e4567-e89b-12d3-a456-426614174000" + + val serverLinks = LoginServerLinks( + api = "https://api.example.com", + accounts = "https://accounts.example.com", + webSocket = "wss://websocket.example.com", + blackList = "https://blacklist.example.com", + teams = "https://teams.example.com", + website = "https://www.example.com", + title = "Example", + isOnPremises = false, + ) + + object OpenUrlBackend : LoginSsoBackend { + override suspend fun initiateLogin(ssoCode: String): LoginSsoBackendResult = + LoginSsoBackendResult.OpenUrl( + url = "https://accounts.example.com/sso", + serverLinks = serverLinks, + ) + + override suspend fun completeLogin( + cookie: String, + serverConfigId: String, + ): LoginSsoBackendResult = + LoginSsoBackendResult.Error(LoginSsoError.GenericError()) + } + + object SuccessBackend : LoginSsoBackend { + override suspend fun initiateLogin(ssoCode: String): LoginSsoBackendResult = + LoginSsoBackendResult.Error(LoginSsoError.GenericError()) + + override suspend fun completeLogin( + cookie: String, + serverConfigId: String, + ): LoginSsoBackendResult = + LoginSsoBackendResult.Success(initialSyncCompleted = false, e2eiRequired = false) + } + } +} diff --git a/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeContractTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeContractTest.kt new file mode 100644 index 00000000000..9f02208e6ab --- /dev/null +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeContractTest.kt @@ -0,0 +1,79 @@ +/* + * 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.shared.auth.welcome + +import com.wire.shared.auth.login.model.LoginApiProxy +import com.wire.shared.auth.login.model.LoginServerLinks +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class WelcomeContractTest { + + @Test + fun givenServerLinksWithoutProxy_whenCheckingProxy_thenProxyIsDisabled() { + assertFalse(defaultLinks.isProxyEnabled) + } + + @Test + fun givenServerLinksWithProxy_whenCheckingProxy_thenProxyIsEnabled() { + val links = defaultLinks.copy( + apiProxy = LoginApiProxy( + needsAuthentication = true, + host = "proxy.example.com", + port = 8080, + ), + ) + + assertTrue(links.isProxyEnabled) + } + + @Test + fun givenWelcomeState_whenCreatedWithDefaults_thenMatchesAndroidWelcomeDefaults() { + val state = WelcomeState(links = defaultLinks) + + assertEquals(defaultLinks, state.links) + assertFalse(state.isThereActiveSession) + assertFalse(state.maxAccountsReached) + assertFalse(state.nomadAccountBlocksLogin) + assertTrue(state.isAccountCreationAllowed) + assertTrue(state.useNewRegistration) + } + + @Test + fun givenLoginEffect_whenCreated_thenCarriesServerLinksForNextFlow() { + val effect = WelcomeEffect.NavigateToLogin(defaultLinks) + + assertEquals(defaultLinks, effect.links) + } + + private companion object { + val defaultLinks = LoginServerLinks( + api = "https://prod-nginz-https.wire.com", + accounts = "https://account.wire.com", + webSocket = "https://prod-nginz-ssl.wire.com", + blackList = "https://clientblacklist.wire.com/prod", + teams = "https://teams.wire.com", + website = "https://wire.com", + title = "production", + isOnPremises = false, + apiProxy = null, + ) + } +} diff --git a/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactoryTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactoryTest.kt new file mode 100644 index 00000000000..2a21930bac5 --- /dev/null +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactoryTest.kt @@ -0,0 +1,84 @@ +/* + * 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.shared.auth.welcome + +import com.wire.shared.auth.SharedAuthConfig +import com.wire.shared.auth.login.model.LoginApiProxy +import com.wire.shared.auth.login.model.LoginServerLinks +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class WelcomeViewModelFactoryTest { + @Test + fun givenConfig_whenCreatingViewModel_thenStateUsesConfig() { + val viewModel = WelcomeViewModelFactory( + config = SharedAuthConfig( + defaultServerLinks = serverLinks, + maxAccountsReached = true, + ) + ).create() + + assertEquals(serverLinks, viewModel.state.value.links) + assertEquals(true, viewModel.state.value.maxAccountsReached) + } + + @Test + fun givenLoginClicked_whenSendingIntent_thenNavigateToLoginEffectIsEmitted() = runTest { + val viewModel = WelcomeViewModelFactory(SharedAuthConfig(serverLinks)).create() + val effect = async(start = CoroutineStart.UNDISPATCHED) { viewModel.effects.first() } + + viewModel.sendIntent(WelcomeIntent.LoginClicked) + + assertEquals(WelcomeEffect.NavigateToLogin(serverLinks), effect.await()) + } + + @Test + fun givenProxyConfigured_whenCreatingPersonalAccount_thenProxyLimitationEffectIsEmitted() = runTest { + val links = serverLinks.copy( + apiProxy = LoginApiProxy( + needsAuthentication = true, + host = "proxy.example.com", + port = 8080, + ) + ) + val viewModel = WelcomeViewModelFactory(SharedAuthConfig(links)).create() + val effect = async(start = CoroutineStart.UNDISPATCHED) { viewModel.effects.first() } + + viewModel.sendIntent(WelcomeIntent.CreatePersonalAccountClicked) + + assertIs(effect.await()) + } + + private companion object { + val serverLinks = LoginServerLinks( + api = "https://api.example.com", + accounts = "https://accounts.example.com", + webSocket = "wss://websocket.example.com", + blackList = "https://blacklist.example.com", + teams = "https://teams.example.com", + website = "https://www.example.com", + title = "Example", + isOnPremises = false, + ) + } +} diff --git a/shared/export-ios/build.gradle.kts b/shared/export-ios/build.gradle.kts new file mode 100644 index 00000000000..cfc68da0a98 --- /dev/null +++ b/shared/export-ios/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id(libs.plugins.wire.kmp.library.get().pluginId) + alias(libs.plugins.metro) +} + +kotlin { + android { + namespace = "com.wire.ios.shared" + } + + sourceSets { + val commonMain by getting { + dependencies { + api(projects.shared.auth) + api(libs.coroutines.core) + implementation("com.wire.kalium:kalium-logic") + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(libs.coroutines.test) + } + } + } + + targets.withType().configureEach { + binaries.framework { + baseName = "WireIosShared" + isStatic = false + export(projects.shared.auth) + } + } +} diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt new file mode 100644 index 00000000000..646e8826e20 --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt @@ -0,0 +1,168 @@ +/* + * 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.ios.shared + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +/** + * Swift-facing cancellation token returned by observation APIs. + * + * The iOS adapter should keep the returned instance for as long as it wants to receive + * callbacks and call [close] when the subscription is no longer needed. Calling [close] + * multiple times is safe. + */ +interface IosCloseable { + fun close() +} + +/** + * Swift-facing ViewModel bridge. + * + * The UI observes [state], handles one-shot [effects], sends user actions with [sendIntent], + * and calls [close] when the Swift owner is deallocated. + */ +class IosViewModel( + /** + * Reactive UI state stream. + * + * Compose can collect this directly. Swift should usually prefer a typed screen wrapper + * and its [IosObservableViewModel.currentState] / [IosObservableViewModel.observeState] + * APIs instead of working with Kotlin Flow types directly. + */ + val state: StateFlow, + /** + * One-shot UI effects stream, such as navigation, opening URLs, or transient messages. + * + * Effects are not part of the durable screen state. Swift should usually subscribe through + * [IosObservableViewModel.observeEffect]. + */ + val effects: Flow, + private val onIntent: (Intent) -> Unit, + private val onClose: () -> Unit = {}, +) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private var isClosed = false + + /** + * Synchronous snapshot of the latest state. + * + * Swift adapters should use this to initialize their `@Published` state before subscribing + * to [observeState]. + */ + val currentState: State + get() = state.value + + /** + * Observes state updates until the returned [IosCloseable] is closed or this ViewModel is closed. + * + * The observer receives the current [StateFlow] value first, then subsequent updates. + */ + fun observeState(observer: (State) -> Unit): IosCloseable = + observe(state, observer) + + /** + * Observes one-shot effects until the returned [IosCloseable] is closed or this ViewModel is closed. + * + * Effects should be handled once by the platform UI layer and not stored as durable state. + */ + fun observeEffect(observer: (Effect) -> Unit): IosCloseable = + observe(effects, observer) + + /** + * Sends a user or platform action to the shared ViewModel. + * + * Keep platform-specific payloads out of [Intent]; use platform gateways or effect callbacks + * when the action requires iOS or Android APIs. + */ + fun sendIntent(intent: Intent) { + onIntent(intent) + } + + /** + * Releases resources owned by this bridge. + * + * Swift adapters should call this from `deinit` or their explicit close path. Calling it + * multiple times is safe. + */ + fun close() { + if (isClosed) return + isClosed = true + scope.close() + onClose() + } + + private fun observe( + flow: Flow, + observer: (T) -> Unit, + ): IosCloseable { + val job = scope.launch { + flow.collect(observer) + } + return job.asIosCloseable() + } +} + +/** + * Typed Swift-facing ViewModel contract implemented by per-screen wrappers. + * + * The generic [IosViewModel] remains useful internally and for Compose/KMP tests, while screen + * wrappers expose concrete State/Effect/Intent types to SwiftUI without generic boilerplate. + */ +interface IosObservableViewModel { + /** + * Latest non-null screen state snapshot. + */ + val currentState: State + + /** + * Subscribes to state changes and returns a token that cancels only this subscription. + */ + fun observeState(observer: (State) -> Unit): IosCloseable + + /** + * Subscribes to one-shot effects and returns a token that cancels only this subscription. + */ + fun observeEffect(observer: (Effect) -> Unit): IosCloseable + + /** + * Sends a UI intent to the shared ViewModel. + */ + fun sendIntent(intent: Intent) + + /** + * Closes the ViewModel bridge and all subscriptions owned by it. + */ + fun close() +} + +private fun CoroutineScope.close() { + coroutineContext[Job]?.cancel() +} + +private fun Job.asIosCloseable(): IosCloseable = + object : IosCloseable { + override fun close() { + cancel() + } + } diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt new file mode 100644 index 00000000000..5a4c2bb04f2 --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt @@ -0,0 +1,20 @@ +/* + * 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.ios.shared + +data object NoEffect diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt new file mode 100644 index 00000000000..defda5a4194 --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt @@ -0,0 +1,118 @@ +/* + * 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.ios.shared + +import com.wire.shared.auth.login.model.LoginServerLinks + +data class WireIosSharedConfig( + val defaultServerLinks: LoginServerLinks, + val runtimeConfig: IosKaliumRuntimeConfig? = null, + val isThereActiveSession: Boolean = false, + val maxAccountsReached: Boolean = false, + val nomadAccountBlocksLogin: Boolean = false, + val isAccountCreationAllowed: Boolean = true, + val useNewRegistration: Boolean = true, +) + +data class IosKaliumRuntimeConfig( + val appGroupRootPath: String, + val accountDataPath: String? = null, + val sqlDelightRootPath: String, + val coreCryptoPath: String? = null, + val userId: String? = null, + val clientId: String? = null, + val backendDomain: String, + val serverLinks: LoginServerLinks, + val migrationMode: MigrationMode, +) + +enum class MigrationMode { + CleanInstallProbe, + ExistingIosAccountOpenInPlace, +} + +fun createWireIosSharedConfig(defaultServerLinks: LoginServerLinks): WireIosSharedConfig = + WireIosSharedConfig(defaultServerLinks = defaultServerLinks) + +fun createWireIosSharedConfig( + defaultServerLinks: LoginServerLinks, + runtimeConfig: IosKaliumRuntimeConfig?, +): WireIosSharedConfig = + WireIosSharedConfig( + defaultServerLinks = defaultServerLinks, + runtimeConfig = runtimeConfig, + ) + +@Suppress("LongParameterList") +fun createWireIosSharedConfig( + defaultServerLinks: LoginServerLinks, + runtimeConfig: IosKaliumRuntimeConfig?, + isThereActiveSession: Boolean, + maxAccountsReached: Boolean, + nomadAccountBlocksLogin: Boolean, + isAccountCreationAllowed: Boolean, + useNewRegistration: Boolean, +): WireIosSharedConfig = + WireIosSharedConfig( + defaultServerLinks = defaultServerLinks, + runtimeConfig = runtimeConfig, + isThereActiveSession = isThereActiveSession, + maxAccountsReached = maxAccountsReached, + nomadAccountBlocksLogin = nomadAccountBlocksLogin, + isAccountCreationAllowed = isAccountCreationAllowed, + useNewRegistration = useNewRegistration, + ) + +fun createIosKaliumRuntimeConfig( + appGroupRootPath: String, + sqlDelightRootPath: String, + backendDomain: String, + serverLinks: LoginServerLinks, + migrationMode: MigrationMode, +): IosKaliumRuntimeConfig = + IosKaliumRuntimeConfig( + appGroupRootPath = appGroupRootPath, + sqlDelightRootPath = sqlDelightRootPath, + backendDomain = backendDomain, + serverLinks = serverLinks, + migrationMode = migrationMode, + ) + +@Suppress("LongParameterList") +fun createIosKaliumRuntimeConfig( + appGroupRootPath: String, + accountDataPath: String?, + sqlDelightRootPath: String, + coreCryptoPath: String?, + userId: String?, + clientId: String?, + backendDomain: String, + serverLinks: LoginServerLinks, + migrationMode: MigrationMode, +): IosKaliumRuntimeConfig = + IosKaliumRuntimeConfig( + appGroupRootPath = appGroupRootPath, + accountDataPath = accountDataPath, + sqlDelightRootPath = sqlDelightRootPath, + coreCryptoPath = coreCryptoPath, + userId = userId, + clientId = clientId, + backendDomain = backendDomain, + serverLinks = serverLinks, + migrationMode = migrationMode, + ) diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt new file mode 100644 index 00000000000..171952c6074 --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt @@ -0,0 +1,20 @@ +/* + * 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.ios.shared + +abstract class WireIosSharedScope private constructor() diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridge.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridge.kt new file mode 100644 index 00000000000..5373bc65f6c --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridge.kt @@ -0,0 +1,55 @@ +/* + * 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/. + */ +@file:Suppress("MatchingDeclarationName") + +package com.wire.ios.shared.auth.bridge + +import com.wire.ios.shared.IosViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface AuthBridgeViewModel { + val state: StateFlow + val effects: Flow + + fun sendIntent(intent: Intent) + + fun close() = Unit +} + +fun AuthBridgeViewModel.asIosViewModel(): + IosViewModel = + authIosViewModel( + state = state, + effects = effects, + onIntent = ::sendIntent, + onClose = ::close, + ) + +fun authIosViewModel( + state: StateFlow, + effects: Flow, + onIntent: (Intent) -> Unit, + onClose: () -> Unit = {}, +): IosViewModel = + IosViewModel( + state = state, + effects = effects, + onIntent = onIntent, + onClose = onClose, + ) diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModel.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModel.kt new file mode 100644 index 00000000000..368879bd6ee --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModel.kt @@ -0,0 +1,85 @@ +/* + * 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.ios.shared.auth.email + +import com.wire.ios.shared.IosCloseable +import com.wire.ios.shared.IosObservableViewModel +import com.wire.ios.shared.IosViewModel +import com.wire.shared.auth.email.LoginEmailEffect +import com.wire.shared.auth.email.LoginEmailIntent +import com.wire.shared.auth.email.LoginEmailState +import com.wire.shared.auth.email.LoginEmailViewModelFactory +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.Dispatchers + +class LoginEmailIosViewModel( + private val delegate: IosViewModel, +) : IosObservableViewModel { + val state = delegate.state + val effects = delegate.effects + + override val currentState: LoginEmailState + get() = delegate.currentState + + override fun observeState(observer: (LoginEmailState) -> Unit): IosCloseable = + delegate.observeState(observer) + + override fun observeEffect(observer: (LoginEmailEffect) -> Unit): IosCloseable = + delegate.observeEffect(observer) + + override fun sendIntent(intent: LoginEmailIntent) { + delegate.sendIntent(intent) + } + + override fun close() { + delegate.close() + } +} + +@Inject +class LoginEmailIosViewModelFactory( + private val sharedFactory: LoginEmailViewModelFactory, +) { + fun create(userIdentifier: String = ""): LoginEmailIosViewModel = + LoginEmailIosViewModel(createGeneric(userIdentifier)) + + fun createGeneric(userIdentifier: String = ""): IosViewModel { + val sharedViewModel = sharedFactory.create( + userIdentifier = userIdentifier, + coroutineContext = Dispatchers.Main.immediate, + ) + return IosViewModel( + state = sharedViewModel.state, + effects = sharedViewModel.effects, + onIntent = sharedViewModel::sendIntent, + onClose = sharedViewModel::close, + ) + } +} + +fun createLoginEmailIosViewModel( + loginEmailIosViewModelFactory: LoginEmailIosViewModelFactory, + userIdentifier: String = "", +): LoginEmailIosViewModel = + loginEmailIosViewModelFactory.create(userIdentifier) + +fun createGenericLoginEmailIosViewModel( + loginEmailIosViewModelFactory: LoginEmailIosViewModelFactory, + userIdentifier: String = "", +): IosViewModel = + loginEmailIosViewModelFactory.createGeneric(userIdentifier) diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/flow/AuthLoginFlowIosViewModel.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/flow/AuthLoginFlowIosViewModel.kt new file mode 100644 index 00000000000..ccabd2ddc0d --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/flow/AuthLoginFlowIosViewModel.kt @@ -0,0 +1,80 @@ +/* + * 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.ios.shared.auth.flow + +import com.wire.ios.shared.IosCloseable +import com.wire.ios.shared.IosObservableViewModel +import com.wire.ios.shared.IosViewModel +import com.wire.shared.auth.flow.AuthLoginFlowEffect +import com.wire.shared.auth.flow.AuthLoginFlowIntent +import com.wire.shared.auth.flow.AuthLoginFlowState +import com.wire.shared.auth.flow.AuthLoginFlowViewModelFactory +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.Dispatchers + +class AuthLoginFlowIosViewModel( + private val delegate: IosViewModel, +) : IosObservableViewModel { + val state = delegate.state + val effects = delegate.effects + + override val currentState: AuthLoginFlowState + get() = delegate.currentState + + override fun observeState(observer: (AuthLoginFlowState) -> Unit): IosCloseable = + delegate.observeState(observer) + + override fun observeEffect(observer: (AuthLoginFlowEffect) -> Unit): IosCloseable = + delegate.observeEffect(observer) + + override fun sendIntent(intent: AuthLoginFlowIntent) { + delegate.sendIntent(intent) + } + + override fun close() { + delegate.close() + } +} + +@Inject +class AuthLoginFlowIosViewModelFactory( + private val sharedFactory: AuthLoginFlowViewModelFactory, +) { + fun create(): AuthLoginFlowIosViewModel = + AuthLoginFlowIosViewModel(createGeneric()) + + fun createGeneric(): IosViewModel { + val sharedViewModel = sharedFactory.create(coroutineContext = Dispatchers.Main.immediate) + return IosViewModel( + state = sharedViewModel.state, + effects = sharedViewModel.effects, + onIntent = sharedViewModel::sendIntent, + onClose = sharedViewModel::close, + ) + } +} + +fun createAuthLoginFlowIosViewModel( + authLoginFlowIosViewModelFactory: AuthLoginFlowIosViewModelFactory, +): AuthLoginFlowIosViewModel = + authLoginFlowIosViewModelFactory.create() + +fun createGenericAuthLoginFlowIosViewModel( + authLoginFlowIosViewModelFactory: AuthLoginFlowIosViewModelFactory, +): IosViewModel = + authLoginFlowIosViewModelFactory.createGeneric() diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModel.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModel.kt new file mode 100644 index 00000000000..559d92655cb --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModel.kt @@ -0,0 +1,80 @@ +/* + * 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.ios.shared.auth.newlogin + +import com.wire.ios.shared.IosCloseable +import com.wire.ios.shared.IosObservableViewModel +import com.wire.ios.shared.IosViewModel +import com.wire.shared.auth.newlogin.NewLoginIdentifierEffect +import com.wire.shared.auth.newlogin.NewLoginIdentifierIntent +import com.wire.shared.auth.newlogin.NewLoginIdentifierState +import com.wire.shared.auth.newlogin.NewLoginIdentifierViewModelFactory +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.Dispatchers + +class NewLoginIdentifierIosViewModel( + private val delegate: IosViewModel, +) : IosObservableViewModel { + val state = delegate.state + val effects = delegate.effects + + override val currentState: NewLoginIdentifierState + get() = delegate.currentState + + override fun observeState(observer: (NewLoginIdentifierState) -> Unit): IosCloseable = + delegate.observeState(observer) + + override fun observeEffect(observer: (NewLoginIdentifierEffect) -> Unit): IosCloseable = + delegate.observeEffect(observer) + + override fun sendIntent(intent: NewLoginIdentifierIntent) { + delegate.sendIntent(intent) + } + + override fun close() { + delegate.close() + } +} + +@Inject +class NewLoginIdentifierIosViewModelFactory( + private val sharedFactory: NewLoginIdentifierViewModelFactory, +) { + fun create(): NewLoginIdentifierIosViewModel = + NewLoginIdentifierIosViewModel(createGeneric()) + + fun createGeneric(): IosViewModel { + val sharedViewModel = sharedFactory.create(coroutineContext = Dispatchers.Main.immediate) + return IosViewModel( + state = sharedViewModel.state, + effects = sharedViewModel.effects, + onIntent = sharedViewModel::sendIntent, + onClose = sharedViewModel::close, + ) + } +} + +fun createNewLoginIdentifierIosViewModel( + newLoginIdentifierIosViewModelFactory: NewLoginIdentifierIosViewModelFactory, +): NewLoginIdentifierIosViewModel = + newLoginIdentifierIosViewModelFactory.create() + +fun createGenericNewLoginIdentifierIosViewModel( + newLoginIdentifierIosViewModelFactory: NewLoginIdentifierIosViewModelFactory, +): IosViewModel = + newLoginIdentifierIosViewModelFactory.createGeneric() diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoIosViewModel.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoIosViewModel.kt new file mode 100644 index 00000000000..c2ff135a3a2 --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoIosViewModel.kt @@ -0,0 +1,80 @@ +/* + * 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.ios.shared.auth.sso + +import com.wire.ios.shared.IosCloseable +import com.wire.ios.shared.IosObservableViewModel +import com.wire.ios.shared.IosViewModel +import com.wire.shared.auth.sso.LoginSsoEffect +import com.wire.shared.auth.sso.LoginSsoIntent +import com.wire.shared.auth.sso.LoginSsoState +import com.wire.shared.auth.sso.LoginSsoViewModelFactory +import dev.zacsweers.metro.Inject +import kotlinx.coroutines.Dispatchers + +class LoginSsoIosViewModel( + private val delegate: IosViewModel, +) : IosObservableViewModel { + val state = delegate.state + val effects = delegate.effects + + override val currentState: LoginSsoState + get() = delegate.currentState + + override fun observeState(observer: (LoginSsoState) -> Unit): IosCloseable = + delegate.observeState(observer) + + override fun observeEffect(observer: (LoginSsoEffect) -> Unit): IosCloseable = + delegate.observeEffect(observer) + + override fun sendIntent(intent: LoginSsoIntent) { + delegate.sendIntent(intent) + } + + override fun close() { + delegate.close() + } +} + +@Inject +class LoginSsoIosViewModelFactory( + private val sharedFactory: LoginSsoViewModelFactory, +) { + fun create(): LoginSsoIosViewModel = + LoginSsoIosViewModel(createGeneric()) + + fun createGeneric(): IosViewModel { + val sharedViewModel = sharedFactory.create(coroutineContext = Dispatchers.Main.immediate) + return IosViewModel( + state = sharedViewModel.state, + effects = sharedViewModel.effects, + onIntent = sharedViewModel::sendIntent, + onClose = sharedViewModel::close, + ) + } +} + +fun createLoginSsoIosViewModel( + loginSsoIosViewModelFactory: LoginSsoIosViewModelFactory, +): LoginSsoIosViewModel = + loginSsoIosViewModelFactory.create() + +fun createGenericLoginSsoIosViewModel( + loginSsoIosViewModelFactory: LoginSsoIosViewModelFactory, +): IosViewModel = + loginSsoIosViewModelFactory.createGeneric() diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt new file mode 100644 index 00000000000..385ac6b4226 --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt @@ -0,0 +1,79 @@ +/* + * 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.ios.shared.auth.welcome + +import com.wire.ios.shared.IosCloseable +import com.wire.ios.shared.IosObservableViewModel +import com.wire.ios.shared.IosViewModel +import com.wire.shared.auth.welcome.WelcomeEffect +import com.wire.shared.auth.welcome.WelcomeIntent +import com.wire.shared.auth.welcome.WelcomeState +import com.wire.shared.auth.welcome.WelcomeViewModelFactory +import dev.zacsweers.metro.Inject + +class WelcomeIosViewModel( + private val delegate: IosViewModel, +) : IosObservableViewModel { + val state = delegate.state + val effects = delegate.effects + + override val currentState: WelcomeState + get() = delegate.currentState + + override fun observeState(observer: (WelcomeState) -> Unit): IosCloseable = + delegate.observeState(observer) + + override fun observeEffect(observer: (WelcomeEffect) -> Unit): IosCloseable = + delegate.observeEffect(observer) + + override fun sendIntent(intent: WelcomeIntent) { + delegate.sendIntent(intent) + } + + override fun close() { + delegate.close() + } +} + +@Inject +class WelcomeIosViewModelFactory( + private val sharedFactory: WelcomeViewModelFactory, +) { + fun create(): WelcomeIosViewModel = + WelcomeIosViewModel(createGeneric()) + + fun createGeneric(): IosViewModel { + val sharedViewModel = sharedFactory.create() + return IosViewModel( + state = sharedViewModel.state, + effects = sharedViewModel.effects, + onIntent = sharedViewModel::sendIntent, + onClose = sharedViewModel::close, + ) + } +} + +fun createWelcomeIosViewModel( + welcomeIosViewModelFactory: WelcomeIosViewModelFactory, +): WelcomeIosViewModel = + welcomeIosViewModelFactory.create() + +fun createGenericWelcomeIosViewModel( + welcomeIosViewModelFactory: WelcomeIosViewModelFactory, +): IosViewModel = + welcomeIosViewModelFactory.createGeneric() diff --git a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt new file mode 100644 index 00000000000..735ad37016e --- /dev/null +++ b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt @@ -0,0 +1,107 @@ +/* + * 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.ios.shared + +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class IosViewModelTest { + @Test + fun givenState_whenCurrentStateIsRead_thenReturnsStateValue() { + val viewModel = IosViewModel( + state = MutableStateFlow(TestState), + effects = MutableSharedFlow(), + onIntent = {}, + ) + + assertEquals(TestState, viewModel.currentState) + } + + @Test + fun givenIntent_whenSendIntent_thenDelegatesToHandler() { + var handledIntent: TestIntent? = null + val viewModel = IosViewModel( + state = MutableStateFlow(TestState), + effects = MutableSharedFlow(), + onIntent = { handledIntent = it }, + ) + + viewModel.sendIntent(TestIntent) + + assertEquals(TestIntent, handledIntent) + } + + @Test + fun givenStateObserver_whenObserving_thenObserverReceivesCurrentState() = runTest { + val state = MutableStateFlow(TestState) + val viewModel = IosViewModel( + state = state, + effects = MutableSharedFlow(), + onIntent = {}, + ) + val initialState = async(start = CoroutineStart.UNDISPATCHED) { + var observedState: TestState? = null + val closeable = viewModel.observeState { observedState = it } + closeable.close() + observedState + } + + assertEquals(TestState, initialState.await()) + } + + @Test + fun givenEffectObserver_whenEffectEmits_thenObserverReceivesEffect() = runTest { + val effects = MutableSharedFlow(extraBufferCapacity = 1) + val viewModel = IosViewModel( + state = MutableStateFlow(TestState), + effects = effects, + onIntent = {}, + ) + var observedEffect: NoEffect? = null + val closeable = viewModel.observeEffect { observedEffect = it } + + effects.emit(NoEffect) + closeable.close() + + assertEquals(NoEffect, observedEffect) + } + + @Test + fun givenCloseHandler_whenClose_thenDelegatesToHandler() { + var closeCount = 0 + val viewModel = IosViewModel( + state = MutableStateFlow(TestState), + effects = MutableSharedFlow(), + onIntent = {}, + onClose = { closeCount++ }, + ) + + viewModel.close() + viewModel.close() + + assertEquals(1, closeCount) + } + + private data object TestState + private data object TestIntent +} diff --git a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridgeTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridgeTest.kt new file mode 100644 index 00000000000..ac02ae7076f --- /dev/null +++ b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridgeTest.kt @@ -0,0 +1,95 @@ +/* + * 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.ios.shared.auth.bridge + +import com.wire.ios.shared.NoEffect +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow + +class AuthIosBridgeTest { + + @Test + fun givenStateAndEffects_whenAuthIosViewModelIsCreated_thenExposesSameFlows() { + val state = MutableStateFlow(TestState) + val effects = MutableSharedFlow() + + val viewModel = authIosViewModel( + state = state, + effects = effects, + onIntent = {}, + ) + + assertSame(state, viewModel.state) + assertSame(effects, viewModel.effects) + } + + @Test + fun givenIntentAndCloseHandlers_whenBridgeIsUsed_thenDelegatesCalls() { + var handledIntent: TestIntent? = null + var closed = false + val viewModel = authIosViewModel( + state = MutableStateFlow(TestState), + effects = MutableSharedFlow(), + onIntent = { handledIntent = it }, + onClose = { closed = true }, + ) + + viewModel.sendIntent(TestIntent.Submit) + viewModel.close() + + assertEquals(TestIntent.Submit, handledIntent) + assertTrue(closed) + } + + @Test + fun givenAuthBridgeViewModel_whenConverted_thenDelegatesIntentAndClose() { + val bridgeViewModel = TestBridgeViewModel() + val viewModel = bridgeViewModel.asIosViewModel() + + viewModel.sendIntent(TestIntent.Submit) + viewModel.close() + + assertEquals(TestIntent.Submit, bridgeViewModel.handledIntent) + assertTrue(bridgeViewModel.closed) + } + + private data object TestState + + private sealed interface TestIntent { + data object Submit : TestIntent + } + + private class TestBridgeViewModel : AuthBridgeViewModel { + override val state = MutableStateFlow(TestState) + override val effects = MutableSharedFlow() + var handledIntent: TestIntent? = null + var closed = false + + override fun sendIntent(intent: TestIntent) { + handledIntent = intent + } + + override fun close() { + closed = true + } + } +} diff --git a/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailGateway.kt b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailGateway.kt new file mode 100644 index 00000000000..b9a0189e90b --- /dev/null +++ b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailGateway.kt @@ -0,0 +1,366 @@ +/* + * 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.shared.auth.email + +import com.wire.ios.shared.IosKaliumRuntimeConfig +import com.wire.ios.shared.WireIosSharedConfig +import com.wire.shared.auth.AuthLoginSuccessPayload +import com.wire.shared.auth.login.model.toKalium +import com.wire.kalium.logic.CoreLogicCommon +import com.wire.kalium.logic.data.auth.AccountTokens +import com.wire.kalium.logic.data.auth.verification.VerifiableAction +import com.wire.kalium.logic.data.session.StoreSessionParam +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase +import com.wire.kalium.logic.feature.auth.AuthenticationResult +import com.wire.kalium.logic.feature.auth.AuthenticationScope +import com.wire.kalium.logic.feature.auth.PersistSelfUserEmailResult +import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase +import com.wire.kalium.logic.feature.auth.verification.RequestSecondFactorVerificationCodeUseCase +import com.wire.kalium.logic.feature.client.RegisterClientParam +import com.wire.kalium.logic.feature.client.RegisterClientResult +import dev.zacsweers.metro.Inject + +/** + * iOS runtime implementation of the shared login gateway. + * + * This class only bridges the shared auth ViewModel to Kalium. Delete it once the Kalium-backed + * login orchestration is available from common shared auth code and no longer needs iOS runtime + * path/config adaptation in export-ios. + */ +@Suppress("TooManyFunctions") +@Inject +class KaliumLoginEmailGateway( + private val config: WireIosSharedConfig, + private val coreLogic: CoreLogicCommon, +) : LoginEmailGateway { + private val runtimeConfig: IosKaliumRuntimeConfig? + get() = config.runtimeConfig + + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailGatewayResult = withRuntimeConfig { runtime -> + if (!usernameAllowed && !coreLogic.getGlobalScope().validateEmailUseCase(userIdentifier)) { + LoginEmailGatewayResult.Failure(LoginEmailError.InvalidUserIdentifier) + } else { + loginWithRuntime( + runtime = runtime, + userIdentifier = userIdentifier, + password = password, + secondFactorVerificationCode = secondFactorVerificationCode, + ) + } + } + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailGatewayResult = + withRuntimeConfig { runtime -> + when (val result = resolveAuthScope(runtime)) { + is AuthScopeResult.Failure -> LoginEmailGatewayResult.Failure(result.error) + is AuthScopeResult.Success -> requestSecondFactorCode(result.authScope, userIdentifier) + } + } + + private suspend fun withRuntimeConfig( + block: suspend (IosKaliumRuntimeConfig) -> LoginEmailGatewayResult, + ): LoginEmailGatewayResult = + runtimeConfig?.let { block(it) } ?: missingRuntimeConfig() + + private suspend fun loginWithRuntime( + runtime: IosKaliumRuntimeConfig, + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + ): LoginEmailGatewayResult = + when (val result = resolveAuthScope(runtime)) { + is AuthScopeResult.Failure -> LoginEmailGatewayResult.Failure(result.error) + is AuthScopeResult.Success -> authenticate( + runtime = runtime, + authScope = result.authScope, + userIdentifier = userIdentifier, + password = password, + secondFactorVerificationCode = secondFactorVerificationCode, + ) + } + + private suspend fun authenticate( + runtime: IosKaliumRuntimeConfig, + authScope: AuthenticationScope, + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + ): LoginEmailGatewayResult = + when ( + val result = authScope.login( + userIdentifier = userIdentifier, + password = password, + shouldPersistClient = true, + secondFactorVerificationCode = secondFactorVerificationCode, + ) + ) { + is AuthenticationResult.Failure -> + handleAuthenticationFailure(result, authScope, userIdentifier) + + is AuthenticationResult.Success -> + completeSuccessfulAuthentication( + input = AuthLoginAttemptInput( + runtime = runtime, + loginResult = result, + userIdentifier = userIdentifier, + password = password, + secondFactorVerificationCode = secondFactorVerificationCode, + ) + ) + } + + private suspend fun completeSuccessfulAuthentication(input: AuthLoginAttemptInput): LoginEmailGatewayResult { + val storedUserId = addAuthenticatedUser(input.loginResult) + return if (storedUserId == null) { + LoginEmailGatewayResult.Failure(LoginEmailError.UserAlreadyExists) + } else { + persistEmailAndRegisterClient( + input = input, + storedUserId = storedUserId, + ) + } + } + + private suspend fun persistEmailAndRegisterClient( + input: AuthLoginAttemptInput, + storedUserId: UserId, + ): LoginEmailGatewayResult = + persistEmailIfNeeded(storedUserId, input.userIdentifier)?.let { error -> + LoginEmailGatewayResult.Failure(error) + } ?: registerClient( + input = input, + storedUserId = storedUserId, + ) + + private suspend fun persistEmailIfNeeded( + storedUserId: UserId, + userIdentifier: String, + ): LoginEmailError? { + if (coreLogic.getGlobalScope().validateEmailUseCase(userIdentifier)) { + val persistEmailResult = coreLogic.getSessionScope(storedUserId).users.persistSelfUserEmail(userIdentifier) + if (persistEmailResult is PersistSelfUserEmailResult.Failure) { + return LoginEmailError.Generic(persistEmailResult.coreFailure.toString()) + } + } + return null + } + + private suspend fun registerClient( + input: AuthLoginAttemptInput, + storedUserId: UserId, + ): LoginEmailGatewayResult = + when ( + val clientResult = coreLogic.getSessionScope(storedUserId).client.getOrRegister( + RegisterClientParam(password = input.password, capabilities = null) + ) + ) { + is RegisterClientResult.Success -> + LoginEmailGatewayResult.Success( + initialSyncCompleted = false, + isE2EIRequired = false, + payload = input.loginResult.authData.toSuccessPayload( + input = input.toSuccessPayloadInput( + isE2EIRequired = false, + clientId = clientResult.client.id.value, + ), + ), + ) + + is RegisterClientResult.E2EICertificateRequired -> + LoginEmailGatewayResult.Success( + initialSyncCompleted = false, + isE2EIRequired = true, + payload = input.loginResult.authData.toSuccessPayload( + input = input.toSuccessPayloadInput( + isE2EIRequired = true, + clientId = clientResult.client.id.value, + ), + ), + ) + + is RegisterClientResult.Failure.TooManyClients -> + LoginEmailGatewayResult.RemoveDeviceNeeded + + is RegisterClientResult.Failure -> + LoginEmailGatewayResult.Failure(clientResult.toLoginEmailError()) + } + + private suspend fun resolveAuthScope(runtime: IosKaliumRuntimeConfig): AuthScopeResult = + when (val result = coreLogic.versionedAuthenticationScope(runtime.serverLinks.toKalium()).invoke(null)) { + is AutoVersionAuthScopeUseCase.Result.Success -> AuthScopeResult.Success(result.authenticationScope) + is AutoVersionAuthScopeUseCase.Result.Failure -> AuthScopeResult.Failure(result.toLoginEmailError()) + } + + private suspend fun addAuthenticatedUser(loginResult: AuthenticationResult.Success): UserId? = + when ( + val addResult = coreLogic.getGlobalScope().addAuthenticatedAccount( + session = StoreSessionParam( + accountTokens = loginResult.authData, + ssoId = loginResult.ssoID, + managedBy = loginResult.managedBy, + serverConfigId = loginResult.serverConfigId, + proxyCredentials = loginResult.proxyCredentials, + isPersistentWebSocketEnabled = false, + ), + replace = false, + ) + ) { + is AddAuthenticatedUserUseCase.Result.Success -> addResult.userId + is AddAuthenticatedUserUseCase.Result.Failure -> null + } + + private suspend fun handleAuthenticationFailure( + failure: AuthenticationResult.Failure, + authScope: AuthenticationScope, + userIdentifier: String, + ): LoginEmailGatewayResult = + when (failure) { + AuthenticationResult.Failure.InvalidCredentials.Missing2FA -> + requestSecondFactorCode(authScope, userIdentifier) + + AuthenticationResult.Failure.InvalidCredentials.Invalid2FA -> + LoginEmailGatewayResult.SecondFactorRequired( + email = userIdentifier, + isCurrentCodeInvalid = true, + ) + + else -> LoginEmailGatewayResult.Failure(failure.toLoginEmailError()) + } + + private suspend fun requestSecondFactorCode( + authScope: AuthenticationScope, + userIdentifier: String, + ): LoginEmailGatewayResult = + if (userIdentifier.contains("@")) { + requestSecondFactorCodeForEmail(authScope, userIdentifier) + } else { + LoginEmailGatewayResult.Failure(LoginEmailError.RequestSecondFactorWithHandle) + } + + private suspend fun requestSecondFactorCodeForEmail( + authScope: AuthenticationScope, + userIdentifier: String, + ): LoginEmailGatewayResult = + when ( + val result = authScope.requestSecondFactorVerificationCode( + email = userIdentifier, + verifiableAction = VerifiableAction.LOGIN_OR_CLIENT_REGISTRATION, + ) + ) { + RequestSecondFactorVerificationCodeUseCase.Result.Success, + RequestSecondFactorVerificationCodeUseCase.Result.Failure.TooManyRequests -> + LoginEmailGatewayResult.SecondFactorRequired(email = userIdentifier) + + is RequestSecondFactorVerificationCodeUseCase.Result.Failure.Generic -> + LoginEmailGatewayResult.Failure(LoginEmailError.Generic(result.cause.toString())) + + else -> LoginEmailGatewayResult.Failure(LoginEmailError.Generic()) + } +} + +private data class AuthLoginAttemptInput( + val runtime: IosKaliumRuntimeConfig, + val loginResult: AuthenticationResult.Success, + val userIdentifier: String, + val password: String, + val secondFactorVerificationCode: String?, +) + +private data class AuthLoginSuccessPayloadInput( + val runtime: IosKaliumRuntimeConfig, + val userIdentifier: String, + val password: String, + val secondFactorVerificationCode: String?, + val isE2EIRequired: Boolean, + val clientId: String?, +) + +private fun AuthLoginAttemptInput.toSuccessPayloadInput( + isE2EIRequired: Boolean, + clientId: String?, +): AuthLoginSuccessPayloadInput = + AuthLoginSuccessPayloadInput( + runtime = runtime, + userIdentifier = userIdentifier, + password = password, + secondFactorVerificationCode = secondFactorVerificationCode, + isE2EIRequired = isE2EIRequired, + clientId = clientId, + ) + +private fun AccountTokens.toSuccessPayload( + input: AuthLoginSuccessPayloadInput, +): AuthLoginSuccessPayload = + AuthLoginSuccessPayload( + userIdValue = userId.value, + userIdDomain = userId.domain.ifBlank { null }, + accessTokenValue = accessToken.value, + accessTokenType = accessToken.tokenType, + accessTokenExpiresInSeconds = null, + refreshTokenValue = refreshToken.value, + refreshTokenCookieDomain = input.runtime.backendDomain, + email = input.userIdentifier, + password = input.password, + secondFactorCode = input.secondFactorVerificationCode, + initialSyncCompleted = false, + isE2EIRequired = input.isE2EIRequired, + clientId = input.clientId, + ) + +private sealed interface AuthScopeResult { + data class Success(val authScope: AuthenticationScope) : AuthScopeResult + data class Failure(val error: LoginEmailError) : AuthScopeResult +} + +private fun AutoVersionAuthScopeUseCase.Result.Failure.toLoginEmailError(): LoginEmailError = + when (this) { + is AutoVersionAuthScopeUseCase.Result.Failure.Generic -> LoginEmailError.Generic(genericFailure.toString()) + AutoVersionAuthScopeUseCase.Result.Failure.TooNewVersion -> LoginEmailError.ClientUpdateRequired + AutoVersionAuthScopeUseCase.Result.Failure.UnknownServerVersion -> LoginEmailError.ServerVersionNotSupported + } + +private fun AuthenticationResult.Failure.toLoginEmailError(): LoginEmailError = + when (this) { + AuthenticationResult.Failure.AccountPendingActivation -> LoginEmailError.AccountPendingActivation + AuthenticationResult.Failure.AccountSuspended -> LoginEmailError.AccountSuspended + AuthenticationResult.Failure.InvalidCredentials.Invalid2FA -> LoginEmailError.InvalidCredentials + AuthenticationResult.Failure.InvalidCredentials.InvalidPasswordIdentityCombination -> LoginEmailError.InvalidCredentials + AuthenticationResult.Failure.InvalidCredentials.Missing2FA -> LoginEmailError.RequestSecondFactorWithHandle + AuthenticationResult.Failure.InvalidUserIdentifier -> LoginEmailError.InvalidUserIdentifier + AuthenticationResult.Failure.SocketError -> LoginEmailError.Generic() + is AuthenticationResult.Failure.Generic -> LoginEmailError.Generic(genericFailure.toString()) + } + +private fun RegisterClientResult.Failure.toLoginEmailError(): LoginEmailError = + when (this) { + is RegisterClientResult.Failure.Generic -> LoginEmailError.Generic(genericFailure.toString()) + is RegisterClientResult.Failure.InvalidCredentials -> LoginEmailError.InvalidCredentials + is RegisterClientResult.Failure.PasswordAuthRequired -> LoginEmailError.PasswordNeededToRegisterClient + is RegisterClientResult.Failure.TooManyClients -> LoginEmailError.TooManyDevices + } + +private fun missingRuntimeConfig(): LoginEmailGatewayResult = + LoginEmailGatewayResult.Failure(LoginEmailError.Generic(MISSING_RUNTIME_CONFIG_ERROR)) + +private const val MISSING_RUNTIME_CONFIG_ERROR = "Kalium runtime config is required for shared iOS login" diff --git a/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/flow/KaliumAuthLoginFlowBackend.kt b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/flow/KaliumAuthLoginFlowBackend.kt new file mode 100644 index 00000000000..72ab84aef6a --- /dev/null +++ b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/flow/KaliumAuthLoginFlowBackend.kt @@ -0,0 +1,114 @@ +/* + * 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.shared.auth.flow + +import com.wire.shared.auth.email.LoginEmailGateway +import com.wire.shared.auth.email.LoginEmailGatewayResult +import com.wire.shared.auth.email.LoginEmailError +import com.wire.shared.auth.newlogin.NewLoginIdentifierBackend +import com.wire.shared.auth.newlogin.NewLoginIdentifierBackendResult +import com.wire.shared.auth.newlogin.NewLoginIdentifierDialogError +import dev.zacsweers.metro.Inject + +@Inject +class KaliumAuthLoginFlowBackend( + private val identifierBackend: NewLoginIdentifierBackend, + private val emailGateway: LoginEmailGateway, +) : AuthLoginFlowBackend { + override suspend fun resolveIdentifier(identifier: String): AuthLoginFlowIdentifierResult = + identifierBackend.resolveEmail(identifier).toFlowIdentifierResult() + + override suspend fun initiateSso(ssoCode: String): AuthLoginFlowIdentifierResult = + identifierBackend.initiateSso(ssoCode).toFlowIdentifierResult() + + override suspend fun loginWithEmail( + identifier: String, + password: String, + secondFactorCode: String?, + usernameAllowed: Boolean, + ): AuthLoginFlowLoginResult = + emailGateway.login( + userIdentifier = identifier, + password = password, + secondFactorVerificationCode = secondFactorCode, + usernameAllowed = usernameAllowed, + ).toFlowLoginResult() +} + +private fun NewLoginIdentifierBackendResult.toFlowIdentifierResult(): AuthLoginFlowIdentifierResult = + when (this) { + is NewLoginIdentifierBackendResult.OpenEmailPassword -> + AuthLoginFlowIdentifierResult.EmailCredentialsRequired(userIdentifier) + + is NewLoginIdentifierBackendResult.OpenSso -> + AuthLoginFlowIdentifierResult.OpenSso( + url = url, + userIdentifier = config.userIdentifier, + ) + + is NewLoginIdentifierBackendResult.Error -> + AuthLoginFlowIdentifierResult.Failure(error.toFlowError()) + + is NewLoginIdentifierBackendResult.EnterpriseLoginNotSupported, + is NewLoginIdentifierBackendResult.OpenCustomConfig -> + AuthLoginFlowIdentifierResult.Failure(AuthLoginFlowError.Generic()) + } + +private fun LoginEmailGatewayResult.toFlowLoginResult(): AuthLoginFlowLoginResult = + when (this) { + is LoginEmailGatewayResult.Success -> + AuthLoginFlowLoginResult.Success( + initialSyncCompleted = initialSyncCompleted, + isE2EIRequired = isE2EIRequired, + payload = payload, + ) + + is LoginEmailGatewayResult.SecondFactorRequired -> + AuthLoginFlowLoginResult.SecondFactorRequired( + email = email, + isCurrentCodeInvalid = isCurrentCodeInvalid, + ) + + LoginEmailGatewayResult.RemoveDeviceNeeded -> + AuthLoginFlowLoginResult.RemoveDeviceNeeded + + is LoginEmailGatewayResult.Failure -> + AuthLoginFlowLoginResult.Failure(error.toFlowError()) + } + +private fun NewLoginIdentifierDialogError.toFlowError(): AuthLoginFlowError = + when (this) { + NewLoginIdentifierDialogError.InvalidSSOCode, + NewLoginIdentifierDialogError.InvalidSSOCookie -> + AuthLoginFlowError.InvalidIdentifier + + else -> + AuthLoginFlowError.Generic() + } + +private fun LoginEmailError.toFlowError(): AuthLoginFlowError = + when (this) { + LoginEmailError.InvalidCredentials -> + AuthLoginFlowError.InvalidCredentials + + LoginEmailError.TooManyDevices -> + AuthLoginFlowError.TooManyDevices + + else -> + AuthLoginFlowError.Generic() + } diff --git a/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinksKaliumMapper.kt b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinksKaliumMapper.kt new file mode 100644 index 00000000000..e46943692f6 --- /dev/null +++ b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinksKaliumMapper.kt @@ -0,0 +1,58 @@ +/* + * 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.shared.auth.login.model + +import com.wire.kalium.logic.configuration.server.ServerConfig + +internal fun LoginServerLinks.toKalium(): ServerConfig.Links = + ServerConfig.Links( + api = api, + accounts = accounts, + webSocket = webSocket, + blackList = blackList, + teams = teams, + website = website, + title = title, + isOnPremises = isOnPremises, + apiProxy = apiProxy?.let { + ServerConfig.ApiProxy( + needsAuthentication = it.needsAuthentication, + host = it.host, + port = it.port, + ) + }, + ) + +internal fun ServerConfig.Links.toIos(): LoginServerLinks = + LoginServerLinks( + api = api, + accounts = accounts, + webSocket = webSocket, + blackList = blackList, + teams = teams, + website = website, + title = title, + isOnPremises = isOnPremises, + apiProxy = apiProxy?.let { + LoginApiProxy( + needsAuthentication = it.needsAuthentication, + host = it.host, + port = it.port, + ) + }, + ) diff --git a/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/newlogin/KaliumNewLoginIdentifierBackend.kt b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/newlogin/KaliumNewLoginIdentifierBackend.kt new file mode 100644 index 00000000000..918cc262948 --- /dev/null +++ b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/newlogin/KaliumNewLoginIdentifierBackend.kt @@ -0,0 +1,176 @@ +/* + * 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.shared.auth.newlogin + +import com.wire.ios.shared.IosKaliumRuntimeConfig +import com.wire.ios.shared.WireIosSharedConfig +import com.wire.shared.auth.login.model.LoginApiProxy +import com.wire.shared.auth.login.model.LoginServerLinks +import com.wire.kalium.logic.CoreLogicCommon +import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.feature.auth.EnterpriseLoginResult +import com.wire.kalium.logic.feature.auth.LoginRedirectPath +import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase +import com.wire.kalium.logic.feature.auth.sso.SSOInitiateLoginResult +import com.wire.kalium.logic.feature.auth.sso.SSOInitiateLoginUseCase +import dev.zacsweers.metro.Inject + +@Inject +class KaliumNewLoginIdentifierBackend( + private val config: WireIosSharedConfig, + private val coreLogic: CoreLogicCommon, +) : NewLoginIdentifierBackend { + private val runtimeConfig: IosKaliumRuntimeConfig? + get() = config.runtimeConfig + + private val localBackend = LocalNewLoginIdentifierBackend() + + override suspend fun resolveEmail(userIdentifier: String): NewLoginIdentifierBackendResult { + val runtime = runtimeConfig ?: return localBackend.resolveEmail(userIdentifier) + return when (val authScope = coreLogic.versionedAuthenticationScope(runtime.serverLinks.toKalium()).invoke(null)) { + is AutoVersionAuthScopeUseCase.Result.Failure -> + NewLoginIdentifierBackendResult.Error(authScope.toDialogError()) + + is AutoVersionAuthScopeUseCase.Result.Success -> + when (val result = authScope.authenticationScope.getLoginFlowForDomainUseCase(userIdentifier)) { + is EnterpriseLoginResult.Failure.Generic -> + NewLoginIdentifierBackendResult.Error(NewLoginIdentifierDialogError.GenericError(result.coreFailure.toString())) + + EnterpriseLoginResult.Failure.NotSupported -> + NewLoginIdentifierBackendResult.EnterpriseLoginNotSupported(userIdentifier) + + is EnterpriseLoginResult.Success -> + result.loginRedirectPath.toBackendResult(userIdentifier) + } + } + } + + override suspend fun initiateSso(ssoCode: String): NewLoginIdentifierBackendResult { + val runtime = runtimeConfig ?: return localBackend.initiateSso(ssoCode) + return when (val authScope = coreLogic.versionedAuthenticationScope(runtime.serverLinks.toKalium()).invoke(null)) { + is AutoVersionAuthScopeUseCase.Result.Failure -> + NewLoginIdentifierBackendResult.Error(authScope.toDialogError()) + + is AutoVersionAuthScopeUseCase.Result.Success -> + when ( + val result = authScope.authenticationScope.ssoLoginScope.initiate( + SSOInitiateLoginUseCase.Param.WithRedirect(ssoCode) + ) + ) { + SSOInitiateLoginResult.Failure.InvalidCode, + SSOInitiateLoginResult.Failure.InvalidCodeFormat -> + NewLoginIdentifierBackendResult.Error(NewLoginIdentifierDialogError.InvalidSSOCode) + + SSOInitiateLoginResult.Failure.InvalidRedirect -> + NewLoginIdentifierBackendResult.Error(NewLoginIdentifierDialogError.GenericError("Invalid SSO redirect")) + + is SSOInitiateLoginResult.Failure.Generic -> + NewLoginIdentifierBackendResult.Error(NewLoginIdentifierDialogError.GenericError(result.genericFailure.toString())) + + is SSOInitiateLoginResult.Success -> + NewLoginIdentifierBackendResult.OpenSso( + url = result.requestUrl, + config = NewLoginSsoUrlConfig(userIdentifier = ssoCode), + ) + } + } + } +} + +private fun AutoVersionAuthScopeUseCase.Result.Failure.toDialogError(): NewLoginIdentifierDialogError = + when (this) { + AutoVersionAuthScopeUseCase.Result.Failure.TooNewVersion -> NewLoginIdentifierDialogError.ClientUpdateRequired + AutoVersionAuthScopeUseCase.Result.Failure.UnknownServerVersion -> NewLoginIdentifierDialogError.ServerVersionNotSupported + is AutoVersionAuthScopeUseCase.Result.Failure.Generic -> NewLoginIdentifierDialogError.GenericError(genericFailure.toString()) + } + +private fun LoginRedirectPath.toBackendResult(userIdentifier: String): NewLoginIdentifierBackendResult = + when (this) { + is LoginRedirectPath.CustomBackend -> + NewLoginIdentifierBackendResult.OpenCustomConfig( + userIdentifier = userIdentifier, + serverLinks = serverLinks.toIos(), + ) + + LoginRedirectPath.Default, + LoginRedirectPath.NoRegistration -> + NewLoginIdentifierBackendResult.OpenEmailPassword( + userIdentifier = userIdentifier, + path = NewLoginPasswordPath( + isCloudAccountCreationPossible = isCloudAccountCreationPossible, + ), + ) + + is LoginRedirectPath.ExistingAccountWithClaimedDomain -> + NewLoginIdentifierBackendResult.OpenEmailPassword( + userIdentifier = userIdentifier, + path = NewLoginPasswordPath( + isCloudAccountCreationPossible = isCloudAccountCreationPossible, + domainClaimedByOrg = NewLoginDomainClaimedByOrg.Claimed(domain), + ), + ) + + is LoginRedirectPath.SSO -> + NewLoginIdentifierBackendResult.OpenSso( + url = ssoCode.withWireSsoPrefix(), + config = NewLoginSsoUrlConfig(userIdentifier = userIdentifier), + ) + } + +private fun LoginServerLinks.toKalium(): ServerConfig.Links = + ServerConfig.Links( + api = api, + accounts = accounts, + webSocket = webSocket, + blackList = blackList, + teams = teams, + website = website, + title = title, + isOnPremises = isOnPremises, + apiProxy = apiProxy?.let { + ServerConfig.ApiProxy( + needsAuthentication = it.needsAuthentication, + host = it.host, + port = it.port, + ) + }, + ) + +private fun ServerConfig.Links.toIos(): LoginServerLinks = + LoginServerLinks( + api = api, + accounts = accounts, + webSocket = webSocket, + blackList = blackList, + teams = teams, + website = website, + title = title, + isOnPremises = isOnPremises, + apiProxy = apiProxy?.let { + LoginApiProxy( + needsAuthentication = it.needsAuthentication, + host = it.host, + port = it.port, + ) + }, + ) + +private fun String.withWireSsoPrefix(): String = + if (startsWith(SSO_CODE_PREFIX)) this else "$SSO_CODE_PREFIX$this" + +private const val SSO_CODE_PREFIX = "wire-" diff --git a/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt new file mode 100644 index 00000000000..a4002929c2d --- /dev/null +++ b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt @@ -0,0 +1,233 @@ +/* + * 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.ios.shared.export + +import com.wire.ios.shared.IosKaliumRuntimeConfig +import com.wire.ios.shared.IosViewModel +import com.wire.ios.shared.MigrationMode +import com.wire.ios.shared.WireIosSharedConfig +import com.wire.ios.shared.WireIosSharedScope +import com.wire.shared.auth.email.KaliumLoginEmailGateway +import com.wire.shared.auth.email.LoginEmailGateway +import com.wire.shared.auth.email.LoginEmailEffect +import com.wire.shared.auth.email.LoginEmailIntent +import com.wire.shared.auth.email.LoginEmailState +import com.wire.ios.shared.auth.email.LoginEmailIosViewModel +import com.wire.ios.shared.auth.email.LoginEmailIosViewModelFactory +import com.wire.ios.shared.auth.email.createGenericLoginEmailIosViewModel +import com.wire.ios.shared.auth.email.createLoginEmailIosViewModel +import com.wire.shared.auth.flow.AuthLoginFlowBackend +import com.wire.shared.auth.flow.AuthLoginFlowEffect +import com.wire.shared.auth.flow.AuthLoginFlowIntent +import com.wire.shared.auth.flow.AuthLoginFlowState +import com.wire.shared.auth.flow.KaliumAuthLoginFlowBackend +import com.wire.ios.shared.auth.flow.AuthLoginFlowIosViewModel +import com.wire.ios.shared.auth.flow.AuthLoginFlowIosViewModelFactory +import com.wire.ios.shared.auth.flow.createAuthLoginFlowIosViewModel +import com.wire.ios.shared.auth.flow.createGenericAuthLoginFlowIosViewModel +import com.wire.shared.auth.login.model.LoginServerLinks +import com.wire.shared.auth.newlogin.KaliumNewLoginIdentifierBackend +import com.wire.shared.auth.newlogin.NewLoginIdentifierBackend +import com.wire.shared.auth.newlogin.NewLoginIdentifierEffect +import com.wire.shared.auth.newlogin.NewLoginIdentifierIntent +import com.wire.shared.auth.newlogin.NewLoginIdentifierState +import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierIosViewModel +import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierIosViewModelFactory +import com.wire.ios.shared.auth.newlogin.createGenericNewLoginIdentifierIosViewModel +import com.wire.ios.shared.auth.newlogin.createNewLoginIdentifierIosViewModel +import com.wire.shared.auth.welcome.WelcomeEffect +import com.wire.shared.auth.welcome.WelcomeIntent +import com.wire.shared.auth.welcome.WelcomeState +import com.wire.ios.shared.auth.welcome.WelcomeIosViewModel +import com.wire.ios.shared.auth.welcome.WelcomeIosViewModelFactory +import com.wire.ios.shared.auth.welcome.createGenericWelcomeIosViewModel +import com.wire.ios.shared.auth.welcome.createWelcomeIosViewModel +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.CoreLogicCommon +import com.wire.kalium.logic.featureFlags.KaliumConfigs +import com.wire.shared.auth.SharedAuthConfig +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.createGraphFactory + +@Suppress("TooManyFunctions") +@DependencyGraph(WireIosSharedScope::class) +interface WireIosSharedGraph { + @DependencyGraph.Factory + fun interface Factory { + fun create(@Provides config: WireIosSharedConfig): WireIosSharedGraph + } + + @SingleIn(WireIosSharedScope::class) + @Provides + fun provideCoreLogic(config: WireIosSharedConfig): CoreLogicCommon { + val runtime = requireNotNull(config.runtimeConfig) { + "IosKaliumRuntimeConfig is required for Kalium-backed iOS login probes." + } + require(runtime.migrationMode == MigrationMode.CleanInstallProbe) { + "Only CleanInstallProbe is supported by the current iOS Kalium login probe." + } + return CoreLogic( + rootPath = runtime.sqlDelightRootPath, + kaliumConfigs = KaliumConfigs(), + userAgent = "WireIosShared/1.0 iOS", + ) + } + + @Provides + fun provideSharedAuthConfig(config: WireIosSharedConfig): SharedAuthConfig = + SharedAuthConfig( + defaultServerLinks = config.defaultServerLinks, + isThereActiveSession = config.isThereActiveSession, + maxAccountsReached = config.maxAccountsReached, + nomadAccountBlocksLogin = config.nomadAccountBlocksLogin, + isAccountCreationAllowed = config.isAccountCreationAllowed, + useNewRegistration = config.useNewRegistration, + ) + + @Provides + fun provideWelcomeIosViewModel( + welcomeIosViewModelFactory: WelcomeIosViewModelFactory, + ): WelcomeIosViewModel = + createWelcomeIosViewModel(welcomeIosViewModelFactory) + + @Provides + fun provideGenericWelcomeIosViewModel( + welcomeIosViewModelFactory: WelcomeIosViewModelFactory, + ): IosViewModel = + createGenericWelcomeIosViewModel(welcomeIosViewModelFactory) + + @Provides + fun provideNewLoginIdentifierViewModel( + newLoginIdentifierIosViewModelFactory: NewLoginIdentifierIosViewModelFactory, + ): NewLoginIdentifierIosViewModel = + createNewLoginIdentifierIosViewModel(newLoginIdentifierIosViewModelFactory) + + @Provides + fun provideGenericNewLoginIdentifierIosViewModel( + newLoginIdentifierIosViewModelFactory: NewLoginIdentifierIosViewModelFactory, + ): IosViewModel = + createGenericNewLoginIdentifierIosViewModel(newLoginIdentifierIosViewModelFactory) + + @Provides + fun provideNewLoginIdentifierBackend( + backend: KaliumNewLoginIdentifierBackend, + ): NewLoginIdentifierBackend = backend + + @Provides + fun provideLoginEmailViewModel( + loginEmailIosViewModelFactory: LoginEmailIosViewModelFactory, + ): LoginEmailIosViewModel = + createLoginEmailIosViewModel(loginEmailIosViewModelFactory) + + @Provides + fun provideGenericLoginEmailIosViewModel( + loginEmailIosViewModelFactory: LoginEmailIosViewModelFactory, + ): IosViewModel = + createGenericLoginEmailIosViewModel(loginEmailIosViewModelFactory) + + @Provides + fun provideLoginEmailGateway( + gateway: KaliumLoginEmailGateway, + ): LoginEmailGateway = gateway + + @Provides + fun provideAuthLoginFlowViewModel( + authLoginFlowIosViewModelFactory: AuthLoginFlowIosViewModelFactory, + ): AuthLoginFlowIosViewModel = + createAuthLoginFlowIosViewModel(authLoginFlowIosViewModelFactory) + + @Provides + fun provideGenericAuthLoginFlowViewModel( + authLoginFlowIosViewModelFactory: AuthLoginFlowIosViewModelFactory, + ): IosViewModel = + createGenericAuthLoginFlowIosViewModel(authLoginFlowIosViewModelFactory) + + @Provides + fun provideAuthLoginFlowBackend( + backend: KaliumAuthLoginFlowBackend, + ): AuthLoginFlowBackend = backend + + val welcomeViewModel: WelcomeIosViewModel + val welcomeIosViewModel: IosViewModel + val newLoginIdentifierViewModel: NewLoginIdentifierIosViewModel + val newLoginIdentifierIosViewModel: IosViewModel + val loginEmailViewModel: LoginEmailIosViewModel + val loginEmailIosViewModel: IosViewModel + val loginEmailViewModelFactory: LoginEmailIosViewModelFactory + val authLoginFlowViewModel: AuthLoginFlowIosViewModel + val authLoginFlowIosViewModel: IosViewModel + + /** + * Releases resources owned by the export graph. + * + * Kalium's CoreLogic currently does not expose a public close hook for its global/session + * providers. ViewModel coroutine scopes are released by each concrete ViewModel's close(). + * iOS should keep this graph alive for the app or debug-probe lifetime instead of creating one + * graph per SwiftUI render or per screen. + */ + fun close() = Unit +} + +fun createWireIosSharedGraph(config: WireIosSharedConfig): WireIosSharedGraph = + createGraphFactory().create(config) + +fun createWireIosShared(defaultServerLinks: LoginServerLinks): WireIosSharedGraph = + createWireIosSharedGraph( + config = com.wire.ios.shared.createWireIosSharedConfig(defaultServerLinks) + ) + +/** + * Creates the shared graph for the first real iOS auth UI slice. + * + * This factory is intended for wiring the existing iOS email/password screen to + * [LoginEmailIosViewModel] while the rest of the production auth flow can remain legacy iOS. + * + * Lifecycle for this phase: + * - keep one graph for the auth debug/app session, not one graph per SwiftUI render; + * - create screen ViewModels from the graph and close each ViewModel when its host adapter is + * deallocated or explicitly closed; + * - call [WireIosSharedGraph.close] when the whole graph/session is discarded. + * + * Storage for this phase: + * - use temporary clean-install paths for [MigrationMode.CleanInstallProbe]; + * - do not point this graph at production `AccountData` until the existing-account integration is + * explicitly enabled and validated in `wire-ios`. + */ +fun createWireIosSharedAuthGraph( + defaultServerLinks: LoginServerLinks, + runtimeConfig: IosKaliumRuntimeConfig, +): WireIosSharedGraph = + createWireIosSharedGraph( + config = com.wire.ios.shared.createWireIosSharedConfig( + defaultServerLinks = defaultServerLinks, + runtimeConfig = runtimeConfig, + ) + ) + +fun createWireIosSharedProbe( + defaultServerLinks: LoginServerLinks, + runtimeConfig: IosKaliumRuntimeConfig? = null, +): WireIosSharedGraph = + createWireIosSharedGraph( + config = com.wire.ios.shared.createWireIosSharedConfig( + defaultServerLinks = defaultServerLinks, + runtimeConfig = runtimeConfig, + ) + ) diff --git a/wireone-kmp/build.gradle.kts b/wireone-kmp/build.gradle.kts index 48fd51ba50b..97fae98f040 100644 --- a/wireone-kmp/build.gradle.kts +++ b/wireone-kmp/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id(libs.plugins.wire.kmp.library.get().pluginId) + alias(libs.plugins.compose.compiler) alias(libs.plugins.jetbrains.compose) } @@ -25,21 +26,15 @@ kotlin { } } - val iosMain by creating { - dependsOn(commonMain) + val iosArm64Main by getting { dependencies { implementation("com.wire.kalium:kalium-logic") } } - - val iosX64Main by getting { - dependsOn(iosMain) - } - val iosArm64Main by getting { - dependsOn(iosMain) - } val iosSimulatorArm64Main by getting { - dependsOn(iosMain) + dependencies { + implementation("com.wire.kalium:kalium-logic") + } } }