From ce88d0a950dc66df98d01c5ae2026a0c733525fc Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 11:55:14 +0200 Subject: [PATCH 01/46] chore: prepare ViewModels for Metro migration --- .../com/wire/android/ui/WireActivity.kt | 6 +- .../android/ui/WireActivityIntentGateway.kt | 68 ++++++++ .../wire/android/ui/WireActivityViewModel.kt | 14 +- .../login/sso/LoginSSOScreen.kt | 14 +- .../sso/LoginSSOSessionExceptionClassifier.kt | 34 ++++ .../login/sso/LoginSSOViewModel.kt | 50 +++--- .../calling/common/SharedCallingViewModel.kt | 10 +- .../ui/calling/incoming/IncomingCallScreen.kt | 6 +- .../ui/calling/ongoing/OngoingCallScreen.kt | 6 +- .../ui/calling/outgoing/OutgoingCallScreen.kt | 6 +- .../android/ui/debug/DebugDataInfoProvider.kt | 50 ++++++ .../ui/debug/DebugDataOptionsViewModel.kt | 12 +- .../com/wire/android/ui/debug/DebugScreen.kt | 2 +- .../debug/ExportObfuscatedCopyFileGateway.kt | 58 +++++++ .../ui/debug/ExportObfuscatedCopyViewModel.kt | 15 +- .../home/conversations/ConversationScreen.kt | 21 ++- .../MessageAttachmentAssetImporter.kt | 39 +++++ .../MessageAttachmentFileGateway.kt | 57 ++++++ .../attachment/MessageAttachmentModule.kt | 34 ++++ .../attachment/MessageAttachmentsViewModel.kt | 42 ++--- .../composer/MessageComposerViewModel.kt | 16 +- .../TempWritableAttachmentUriProvider.kt | 52 ++++++ .../media/ConversationMediaScreen.kt | 2 +- .../preview/ImagesPreviewAssetImporter.kt | 46 +++++ .../media/preview/ImagesPreviewModule.kt | 31 ++++ .../media/preview/ImagesPreviewViewModel.kt | 19 +- .../messages/ConversationAssetFileGateway.kt | 61 +++++++ .../messages/ConversationMessagesViewModel.kt | 20 +-- .../recordaudio/RecordAudioFileGateway.kt | 75 ++++++++ .../recordaudio/RecordAudioViewModel.kt | 30 +--- .../dependencies/DependenciesInfoProvider.kt | 44 +++++ .../dependencies/DependenciesViewModel.kt | 7 +- .../about/licenses/LicensesProvider.kt | 58 +++++++ .../about/licenses/LicensesViewModel.kt | 12 +- .../NetworkSettingsDefaultsProvider.kt | 50 ++++++ .../NetworkSettingsViewModel.kt | 7 +- .../settings/backup/BackupAndRestoreScreen.kt | 4 +- .../backup/BackupAndRestoreViewModel.kt | 26 +-- .../home/settings/backup/BackupFileGateway.kt | 74 ++++++++ .../whatsnew/ReleaseNotesFeedUrlProvider.kt | 47 +++++ .../ui/home/whatsnew/WhatsNewViewModel.kt | 8 +- .../login/NewLoginNavArgsProvider.kt | 30 ++++ ...LoginRecoverableLogoutExceptionDetector.kt | 28 +++ .../login/NewLoginViewModel.kt | 37 ++-- .../about/AboutThisAppInfoProvider.kt | 38 ++++ .../ui/settings/about/AboutThisAppModule.kt | 30 ++++ .../settings/about/AboutThisAppViewModel.kt | 10 +- .../settings/devices/DeviceDetailsScreen.kt | 5 +- .../devices/DeviceDetailsViewModel.kt | 18 +- .../ui/sharing/ImportMediaAssetImporter.kt | 55 ++++++ .../ImportMediaAuthenticatedViewModel.kt | 67 ++----- .../android/ui/sharing/ImportMediaScreen.kt | 4 +- .../ui/sharing/ImportMediaSharingContent.kt | 27 +++ .../sharing/ImportMediaSharingIntentReader.kt | 41 +++++ .../ui/sharing/ImportMediaSharingModule.kt | 31 ++++ .../avatarpicker/AvatarImageGateway.kt | 77 +++++++++ .../userprofile/avatarpicker/AvatarPicker.kt | 10 +- .../avatarpicker/AvatarPickerViewModel.kt | 52 ++---- .../qr/AndroidSelfQRCodeAssetRepository.kt | 53 ++++++ .../qr/SelfQRCodeAssetRepository.kt | 28 +++ .../ui/userprofile/qr/SelfQRCodeModule.kt | 33 ++++ .../ui/userprofile/qr/SelfQRCodeScreen.kt | 18 +- .../ui/userprofile/qr/SelfQRCodeViewModel.kt | 61 ++----- .../service/ServiceDetailsNavArgsProvider.kt | 29 ++++ .../service/ServiceDetailsViewModel.kt | 9 +- .../util/lifecycle/IntentsProcessor.kt | 11 +- .../android/ui/WireActivityViewModelTest.kt | 54 +++--- .../login/sso/LoginSSOViewModelTest.kt | 93 +++++++--- .../ui/calling/SharedCallingViewModelTest.kt | 14 +- .../ExportObfuscatedCopyViewModelTest.kt | 163 ++++++++++++++++++ .../MessageAttachmentsViewModelTest.kt | 107 ++++++++---- .../MessageComposerViewModelArrangement.kt | 34 ++-- .../composer/MessageComposerViewModelTest.kt | 15 ++ .../preview/ImagesPreviewViewModelTest.kt | 129 ++++++++++++++ ...onversationMessagesViewModelArrangement.kt | 16 +- .../ConversationMessagesViewModelTest.kt | 21 ++- .../recordaudio/RecordAudioViewModelTest.kt | 59 +++---- .../dependencies/DependenciesViewModelTest.kt | 59 +++++++ .../about/licenses/LicensesViewModelTest.kt | 63 +++++++ .../NetworkSettingsViewModelTest.kt | 43 ++--- .../home/BackupAndRestoreViewModelTest.kt | 130 ++++++-------- .../ui/home/whatsnew/WhatsNewViewModelTest.kt | 9 +- .../login/NewLoginViewModelTest.kt | 66 ++++--- .../about/AboutThisAppViewModelTest.kt | 53 ++++++ .../debug/DebugDataOptionsViewModelTest.kt | 49 ++++-- .../devices/DeviceDetailsViewModelTest.kt | 17 +- .../ImportMediaAuthenticatedViewModelTest.kt | 112 +++++++++++- .../image/AvatarPickerViewModelTest.kt | 67 +++---- .../userprofile/qr/SelfQRCodeViewModelTest.kt | 45 ++--- .../service/ServiceDetailsViewModelTest.kt | 14 +- .../ui/AndroidCellFileExternalActions.kt | 67 +++++++ .../cells/ui/CellFileExternalActions.kt | 40 +++++ .../cells/ui/CellFileExternalActionsModule.kt | 32 ++++ .../android/feature/cells/ui/CellViewModel.kt | 14 +- .../feature/cells/ui/CellViewModelTest.kt | 51 +++++- .../feature/sketch/DrawingCanvasScreen.kt | 12 +- .../feature/sketch/DrawingCanvasViewModel.kt | 67 +------ .../feature/sketch/SketchImageSaver.kt | 77 +++++++++ .../sketch/DrawingCanvasViewModelTest.kt | 23 +-- 99 files changed, 2982 insertions(+), 868 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/WireActivityIntentGateway.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOSessionExceptionClassifier.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyFileGateway.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentAssetImporter.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentFileGateway.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentModule.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/TempWritableAttachmentUriProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewAssetImporter.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewModule.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationAssetFileGateway.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioFileGateway.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesInfoProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsDefaultsProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupFileGateway.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/whatsnew/ReleaseNotesFeedUrlProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginNavArgsProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginRecoverableLogoutExceptionDetector.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppInfoProvider.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppModule.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAssetImporter.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingContent.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingIntentReader.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingModule.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarImageGateway.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/qr/AndroidSelfQRCodeAssetRepository.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeAssetRepository.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsNavArgsProvider.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModelTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModelTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModelTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModelTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModelTest.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/AndroidCellFileExternalActions.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActions.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActionsModule.kt create mode 100644 features/sketch/src/main/java/com/wire/android/feature/sketch/SketchImageSaver.kt 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..9e9762d878e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -681,16 +681,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) } } 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..0f39a2603eb --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityIntentGateway.kt @@ -0,0 +1,68 @@ +/* + * 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.Binds +import dagger.Lazy +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import javax.inject.Inject + +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 @Inject constructor( + 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), + ) + +@Module +@InstallIn(ViewModelComponent::class) +interface WireActivityIntentGatewayModule { + @Binds + fun bindWireActivityIntentGateway(gateway: AndroidWireActivityIntentGateway): WireActivityIntentGateway +} 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..f434fc36253 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 @@ -125,8 +122,7 @@ class WireActivityViewModel @Inject constructor( 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 +356,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 +387,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/authentication/login/sso/LoginSSOScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt index 98b46e9579b..72810476ad0 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 @@ -81,13 +80,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..4db41279435 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 @@ -26,14 +26,12 @@ 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 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.LoginState import com.wire.android.ui.authentication.login.LoginViewModel import com.wire.android.ui.authentication.login.toLoginError @@ -64,8 +62,6 @@ 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") @@ -79,6 +75,7 @@ class LoginSSOViewModel( userDataStoreProvider: UserDataStoreProvider, private val ssoExtension: LoginSSOViewModelExtension, serverConfig: ServerConfig.Links, + private val sessionExceptionClassifier: LoginSSOSessionExceptionClassifier, private val dispatchers: DispatcherProvider, ) : LoginViewModel( savedStateHandle, @@ -87,9 +84,8 @@ class LoginSSOViewModel( coreLogic, serverConfig ) { - private val loginNavArgs: LoginNavArgs = savedStateHandle.navArgs() - private var pendingNomadServiceUrl: String? = loginNavArgs.ssoCodeAutoLogin?.nomadServiceUrl - private var pendingCookieLabel: String? = loginNavArgs.ssoCodeAutoLogin?.cookieLabel + private var pendingNomadServiceUrl: String? = null + private var pendingCookieLabel: String? = null @Inject constructor( @@ -101,6 +97,7 @@ class LoginSSOViewModel( userDataStoreProvider: UserDataStoreProvider, serverConfig: ServerConfig.Links, @DefaultWebSocketEnabledByDefault defaultWebSocketEnabledByDefault: Boolean, + sessionExceptionClassifier: LoginSSOSessionExceptionClassifier, dispatchers: DispatcherProvider, ) : this( savedStateHandle, @@ -111,6 +108,7 @@ class LoginSSOViewModel( userDataStoreProvider, LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic, defaultWebSocketEnabledByDefault), serverConfig, + sessionExceptionClassifier, dispatchers, ) @@ -184,6 +182,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 +332,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 +367,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 } } } 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..442a8d62e8d 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 @@ -238,18 +236,18 @@ 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) } } 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..a281f0d7e26 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 @@ -64,6 +64,8 @@ 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 @@ -152,7 +154,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 +168,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/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt index 776d3019106..ca414d89d76 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 @@ -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 @@ -256,7 +258,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 +277,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/outgoing/OutgoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt index 6da855f5c49..334bc5f76cc 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 @@ -50,6 +50,8 @@ 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 @@ -93,7 +95,7 @@ fun OutgoingCallScreen( toggleSpeaker = ::toggleSpeaker, toggleVideo = ::toggleVideo, onHangUpCall = outgoingCallViewModel::hangUpCall, - onVideoPreviewCreated = ::setVideoPreview, + onVideoPreviewCreated = { view -> setVideoPreview(PlatformView(view)) }, onSelfClearVideoPreview = ::clearVideoPreview, onCameraPermissionPermanentlyDenied = { permissionPermanentlyDeniedDialogState.show( @@ -107,7 +109,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/debug/DebugDataInfoProvider.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt new file mode 100644 index 00000000000..d25eed7d699 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt @@ -0,0 +1,50 @@ +/* + * 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 dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.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() +} + +@Module +@InstallIn(ViewModelComponent::class) +interface DebugDataInfoProviderModule { + + @Binds + fun bindDebugDataInfoProvider( + provider: AndroidDebugDataInfoProvider + ): DebugDataInfoProvider +} 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..84f6012b873 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 @@ -59,7 +56,6 @@ 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 @@ -97,7 +93,7 @@ interface DebugDataOptionsViewModel { @HiltViewModel class DebugDataOptionsViewModelImpl @Inject constructor( - @ApplicationContext private val context: Context, + private val debugDataInfoProvider: DebugDataInfoProvider, @CurrentAccount val currentAccount: UserId, private val updateApiVersions: UpdateApiVersionsScheduler, private val mlsKeyPackageCount: MLSKeyPackageCountUseCase, @@ -186,11 +182,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/DebugScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt index d9e86cea49c..42ff2992445 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 @@ -168,7 +168,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..58492f742d9 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyFileGateway.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.debug + +import androidx.core.net.toUri +import com.wire.android.util.FileManager +import com.wire.android.util.dispatchers.DispatcherProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import kotlinx.coroutines.withContext +import okio.Path +import javax.inject.Inject + +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) + } +} + +@Module +@InstallIn(ViewModelComponent::class) +interface ExportObfuscatedCopyFileGatewayModule { + @Binds + fun bindExportObfuscatedCopyFileGateway( + gateway: AndroidExportObfuscatedCopyFileGateway + ): ExportObfuscatedCopyFileGateway +} 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..57b512006b0 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,7 +31,6 @@ 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 @@ -40,7 +38,6 @@ 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 @@ -53,7 +50,7 @@ interface ExportObfuscatedCopyViewModel { fun createObfuscatedCopy() {} fun shareCopy() {} - fun saveCopy(uri: Uri) {} + fun saveCopy(destinationUri: String) {} fun cancelBackupCreation() {} } @@ -61,7 +58,7 @@ interface ExportObfuscatedCopyViewModel { class ExportObfuscatedCopyViewModelImpl @OptIn(DelicateKaliumApi::class) @Inject 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 +94,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 +102,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/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 507195c12a8..03ab64570da 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,6 +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.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.LoadState import androidx.paging.PagingData @@ -526,7 +526,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 +542,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 +552,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 +625,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 +666,7 @@ fun ConversationScreen( DrawingCanvasScreenDestination( DrawingCanvasNavArgs( conversationName = conversationInfoViewModel.conversationInfoViewState.conversationName.asString(resources), - tempWritableUri = messageComposerViewModel.tempWritableImageUri + tempWritableUri = messageComposerViewModel.tempWritableImageUri?.toUri() ) ) ) @@ -934,7 +934,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 +957,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 +1074,7 @@ private fun ConversationScreen( onDetailsClick = onMessageDetailsClick, onReplyClick = messageComposerStateHolder::toReply, onEditClick = messageComposerStateHolder::toEdit, - onShareAssetClick = { shareAsset(context, it) }, + onShareAssetClick = shareAsset, onDownloadAssetClick = onDownloadAssetClick, onOpenAssetClick = onOpenAssetClick, ) @@ -1910,7 +1909,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/attachment/MessageAttachmentAssetImporter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentAssetImporter.kt new file mode 100644 index 00000000000..99820282289 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentAssetImporter.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.conversations.attachment + +import androidx.core.net.toUri +import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase +import com.wire.android.ui.sharing.ImportedMediaAsset +import javax.inject.Inject + +interface MessageAttachmentAssetImporter { + suspend fun importAsset(uri: String): ImportedMediaAsset? +} + +class MessageAttachmentAssetImporterImpl @Inject constructor( + 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..508bf3accd8 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentFileGateway.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.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 +import javax.inject.Inject + +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 @Inject constructor( + 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..2242e19e870 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentModule.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.attachment + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +interface MessageAttachmentModule { + + @Binds + fun bindMessageAttachmentAssetImporter(impl: MessageAttachmentAssetImporterImpl): MessageAttachmentAssetImporter + + @Binds + fun bindMessageAttachmentFileGateway(impl: MessageAttachmentFileGatewayImpl): MessageAttachmentFileGateway +} 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..f82f4141645 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,8 +17,6 @@ */ 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 @@ -27,18 +25,13 @@ 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,8 +43,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 @@ -60,21 +51,19 @@ 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, + 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() { @@ -113,27 +102,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 +164,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 +182,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 +243,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 +251,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/composer/MessageComposerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModel.kt index db9ade80fb7..a1e735815a0 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,7 +18,6 @@ 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 @@ -34,10 +33,8 @@ 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 @@ -82,8 +79,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,10 +88,10 @@ 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() @@ -130,15 +126,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/TempWritableAttachmentUriProvider.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/TempWritableAttachmentUriProvider.kt new file mode 100644 index 00000000000..591fee587f7 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/TempWritableAttachmentUriProvider.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.conversations.composer + +import com.wire.android.util.FileManager +import com.wire.kalium.logic.data.asset.KaliumFileSystem +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import javax.inject.Inject + +interface TempWritableAttachmentUriProvider { + suspend fun getTempWritableVideoUri(): String + suspend fun getTempWritableImageUri(): String +} + +class AndroidTempWritableAttachmentUriProvider @Inject constructor( + 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() +} + +@Module +@InstallIn(ViewModelComponent::class) +interface TempWritableAttachmentUriProviderModule { + @Binds + fun bindTempWritableAttachmentUriProvider( + provider: AndroidTempWritableAttachmentUriProvider + ): TempWritableAttachmentUriProvider +} 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..bb8209aee02 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 @@ -125,7 +125,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..52bc40dd41e --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewAssetImporter.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.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 +import javax.inject.Inject + +interface ImagesPreviewAssetImporter { + suspend fun importAsset(uri: String): ImportedMediaAsset? +} + +class ImagesPreviewAssetImporterImpl @Inject constructor( + 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..af79c82c9ce --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewModule.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.media.preview + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +interface ImagesPreviewModule { + + @Binds + fun bindImagesPreviewAssetImporter(impl: ImagesPreviewAssetImporterImpl): ImagesPreviewAssetImporter +} 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..d74419afdca 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,28 +17,22 @@ */ 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 + private val assetImporter: ImagesPreviewAssetImporter ) : ViewModel() { private val navArgs: ImagesPreviewNavArgs = savedStateHandle.navArgs() @@ -65,20 +59,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/messages/ConversationAssetFileGateway.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationAssetFileGateway.kt new file mode 100644 index 00000000000..425d809d35d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationAssetFileGateway.kt @@ -0,0 +1,61 @@ +/* + * 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 dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import okio.Path +import javax.inject.Inject + +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 @Inject constructor( + 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) {} + } +} + +@Module +@InstallIn(ViewModelComponent::class) +interface ConversationAssetFileGatewayModule { + @Binds + fun bindConversationAssetFileGateway(gateway: AndroidConversationAssetFileGateway): ConversationAssetFileGateway +} 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..4ab6c04cfd9 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,7 +18,6 @@ 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 @@ -40,9 +39,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 @@ -97,7 +94,7 @@ class ConversationMessagesViewModel @Inject constructor( 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, @@ -340,7 +337,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 +346,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 +390,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/messagecomposer/recordaudio/RecordAudioFileGateway.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioFileGateway.kt new file mode 100644 index 00000000000..32eed487dfb --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioFileGateway.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.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 dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import okio.Path +import java.io.File +import javax.inject.Inject + +interface RecordAudioFileGateway { + suspend fun generateAudioFileWithEffects( + originalFilePath: String, + effectsFilePath: String + ) + + fun audioLengthInMs(audioPath: Path): Long + fun contentUri(audioFile: File): Uri +} + +class AndroidRecordAudioFileGateway @Inject constructor( + @ApplicationContext 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()) +} + +@Module +@InstallIn(ViewModelComponent::class) +interface RecordAudioFileGatewayModule { + @Binds + fun bindRecordAudioFileGateway(gateway: AndroidRecordAudioFileGateway): RecordAudioFileGateway +} 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..3bbcdcaabf2 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,11 +32,8 @@ 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 @@ -45,7 +41,6 @@ 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 @@ -59,11 +54,10 @@ import kotlin.io.path.deleteIfExists @Suppress("TooManyFunctions", "LongParameterList") @HiltViewModel class RecordAudioViewModel @Inject constructor( - @ApplicationContext private val context: Context, 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 +195,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 +207,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 +310,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 +361,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 +373,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/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..18f496b0613 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesInfoProvider.kt @@ -0,0 +1,44 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.home.settings.about.dependencies + +import android.content.Context +import com.wire.android.util.getDependenciesVersion +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +interface DependenciesInfoProvider { + suspend fun dependenciesVersion(): Map +} + +class AndroidDependenciesInfoProvider @Inject constructor( + @ApplicationContext private val context: Context +) : DependenciesInfoProvider { + override suspend fun dependenciesVersion(): Map = context.getDependenciesVersion() +} + +@Module +@InstallIn(ViewModelComponent::class) +interface DependenciesInfoProviderModule { + @Binds + fun bindDependenciesInfoProvider(provider: AndroidDependenciesInfoProvider): DependenciesInfoProvider +} 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..226e102cf37 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,19 @@ */ 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 + private val dependenciesInfoProvider: DependenciesInfoProvider ) : ViewModel() { var state: DependenciesState by mutableStateOf(DependenciesState()) @@ -44,7 +41,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/licenses/LicensesProvider.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesProvider.kt new file mode 100644 index 00000000000..37af538fc86 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesProvider.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.about.licenses + +import android.content.Context +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.entity.Library +import com.mikepenz.aboutlibraries.util.withContext +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +interface LicensesProvider { + suspend fun getLibraries(): List +} + +class AndroidLicensesProvider @Inject constructor( + @ApplicationContext private val context: Context +) : LicensesProvider { + + override suspend fun getLibraries(): List = withContext(Dispatchers.IO) { + Libs.Builder() + .withContext(context) + .build() + .libraries + .distinctBy { it.uniqueId } + } +} + +@Module +@InstallIn(ViewModelComponent::class) +interface LicensesProviderModule { + + @Binds + fun bindLicensesProvider( + provider: AndroidLicensesProvider + ): LicensesProvider +} 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..df301e703d9 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,26 @@ */ 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 + 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/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..e564b41728d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsDefaultsProvider.kt @@ -0,0 +1,50 @@ +/* + * 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 +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +interface NetworkSettingsDefaultsProvider { + val isWebSocketEnabledByDefault: Boolean +} + +class AndroidNetworkSettingsDefaultsProvider @Inject constructor( + @ApplicationContext private val context: Context +) : NetworkSettingsDefaultsProvider { + + override val isWebSocketEnabledByDefault: Boolean + get() = isWebsocketEnabledByDefault(context) +} + +@Module +@InstallIn(ViewModelComponent::class) +interface NetworkSettingsDefaultsProviderModule { + + @Binds + fun bindNetworkSettingsDefaultsProvider( + provider: AndroidNetworkSettingsDefaultsProvider + ): NetworkSettingsDefaultsProvider +} 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..c22c9607b73 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,13 +25,11 @@ 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 @@ -43,7 +40,7 @@ class NetworkSettingsViewModel 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 +52,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/backup/BackupAndRestoreScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreScreen.kt index a5076747ade..511efee8ac1 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 @@ -71,9 +71,9 @@ fun BackupAndRestoreScreen( 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..179df2963f2 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 @@ -70,8 +67,7 @@ class BackupAndRestoreViewModel @Inject constructor( 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 +150,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 +161,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 +175,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 +287,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/BackupFileGateway.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupFileGateway.kt new file mode 100644 index 00000000000..b29fb7d8f9d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupFileGateway.kt @@ -0,0 +1,74 @@ +/* + * 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 dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import kotlinx.coroutines.withContext +import okio.Path +import javax.inject.Inject + +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) + } + } +} + +@Module +@InstallIn(ViewModelComponent::class) +interface BackupFileGatewayModule { + @Binds + fun bindBackupFileGateway(gateway: AndroidBackupFileGateway): BackupFileGateway +} 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..f491f97f74b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/ReleaseNotesFeedUrlProvider.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.home.whatsnew + +import android.content.Context +import com.wire.android.R +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +interface ReleaseNotesFeedUrlProvider { + val feedUrl: String +} + +class AndroidReleaseNotesFeedUrlProvider @Inject constructor( + @ApplicationContext private val context: Context +) : ReleaseNotesFeedUrlProvider { + override val feedUrl: String + get() = context.resources.getString(R.string.url_android_release_notes_feed) +} + +@Module +@InstallIn(ViewModelComponent::class) +interface ReleaseNotesFeedUrlProviderModule { + @Binds + fun bindReleaseNotesFeedUrlProvider( + provider: AndroidReleaseNotesFeedUrlProvider + ): ReleaseNotesFeedUrlProvider +} 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..8b1c9a0d4db 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,14 +17,12 @@ */ 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 @@ -33,7 +31,9 @@ import java.util.Locale import javax.inject.Inject @HiltViewModel -class WhatsNewViewModel @Inject constructor(context: Context) : ViewModel() { +class WhatsNewViewModel @Inject constructor( + private val releaseNotesFeedUrlProvider: ReleaseNotesFeedUrlProvider +) : ViewModel() { private val rssParser = RssParser() private val publishDateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.ENGLISH) @@ -43,7 +43,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/newauthentication/login/NewLoginNavArgsProvider.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginNavArgsProvider.kt new file mode 100644 index 00000000000..78992dc6c22 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginNavArgsProvider.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.newauthentication.login + +import androidx.lifecycle.SavedStateHandle +import com.ramcosta.composedestinations.generated.app.navArgs +import com.wire.android.ui.authentication.login.LoginNavArgs +import javax.inject.Inject + +class NewLoginNavArgsProvider @Inject constructor( + private val savedStateHandle: SavedStateHandle, +) { + fun loginNavArgs(): LoginNavArgs = savedStateHandle.navArgs() +} 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/NewLoginViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt index a6c7a055207..238b7a9c428 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,7 +18,6 @@ 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 @@ -27,7 +26,6 @@ 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 @@ -71,7 +69,6 @@ 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 @@ -81,6 +78,7 @@ class NewLoginViewModel( private val validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase, val coreLogic: CoreLogic, savedStateHandle: SavedStateHandle, + private val loginNavArgs: LoginNavArgs, val clientScopeProviderFactory: ClientScopeProvider.Factory, val userDataStoreProvider: UserDataStoreProvider, private val loginExtension: LoginViewModelExtension, @@ -88,6 +86,7 @@ class NewLoginViewModel( private val dispatchers: DispatcherProvider, defaultServerConfig: ServerConfig.Links, defaultSSOCodeConfig: String, + private val recoverableLogoutExceptionDetector: NewLoginRecoverableLogoutExceptionDetector, ) : ActionsViewModel() { @Inject @@ -95,6 +94,7 @@ class NewLoginViewModel( validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase, @KaliumCoreLogic coreLogic: CoreLogic, savedStateHandle: SavedStateHandle, + loginNavArgsProvider: NewLoginNavArgsProvider, addAuthenticatedUser: AddAuthenticatedUserUseCase, clientScopeProviderFactory: ClientScopeProvider.Factory, userDataStoreProvider: UserDataStoreProvider, @@ -102,20 +102,22 @@ class NewLoginViewModel( defaultServerConfig: ServerConfig.Links, @Named("ssoCodeConfig") defaultSSOCodeConfig: String, @DefaultWebSocketEnabledByDefault defaultWebSocketEnabledByDefault: Boolean, + recoverableLogoutExceptionDetector: NewLoginRecoverableLogoutExceptionDetector, ) : this( validateEmailOrSSOCode, coreLogic, savedStateHandle, + loginNavArgsProvider.loginNavArgs(), clientScopeProviderFactory, userDataStoreProvider, LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic, defaultWebSocketEnabledByDefault), dispatchers, defaultServerConfig, - defaultSSOCodeConfig + defaultSSOCodeConfig, + recoverableLogoutExceptionDetector ) - 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 @@ -404,15 +406,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 +451,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/settings/about/AboutThisAppInfoProvider.kt b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppInfoProvider.kt new file mode 100644 index 00000000000..a6da3c69986 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppInfoProvider.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.settings.about + +import android.content.Context +import com.wire.android.util.AppNameUtil +import com.wire.android.util.getGitBuildId +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +interface AboutThisAppInfoProvider { + val appName: String + fun gitBuildId(): String +} + +class AndroidAboutThisAppInfoProvider @Inject constructor( + @ApplicationContext 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/AboutThisAppModule.kt b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppModule.kt new file mode 100644 index 00000000000..8648bfec26a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppModule.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.settings.about + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface AboutThisAppModule { + @Binds + fun bindAboutThisAppInfoProvider(provider: AndroidAboutThisAppInfoProvider): AboutThisAppInfoProvider +} 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..b75fc299d35 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,23 @@ */ 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 + private val aboutThisAppInfoProvider: AboutThisAppInfoProvider ) : ViewModel() { var state by mutableStateOf( AboutThisAppState( - appName = AppNameUtil.createAppName() + appName = aboutThisAppInfoProvider.appName ) ) @@ -47,7 +43,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/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index 201241f452e..54b56536371 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 @@ -107,7 +107,10 @@ import kotlinx.datetime.Instant @Composable fun DeviceDetailsScreen( navigator: Navigator, - viewModel: DeviceDetailsViewModel = hiltViewModel() + args: DeviceDetailsNavArgs, + viewModel: DeviceDetailsViewModel = hiltViewModel( + creationCallback = { factory -> factory.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..bdb791f316c 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,18 @@ 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.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.distinctUntilChanged import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel -class DeviceDetailsViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = DeviceDetailsViewModel.Factory::class) +class DeviceDetailsViewModel @AssistedInject constructor( + @Assisted private val deviceDetailsNavArgs: DeviceDetailsNavArgs, @CurrentAccount private val currentUserId: UserId, private val deleteClient: DeleteClientUseCase, @@ -76,7 +76,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 @@ -97,6 +96,11 @@ class DeviceDetailsViewModel @Inject constructor( getIsE2EIEnabled() } + @AssistedFactory + interface Factory { + fun create(args: DeviceDetailsNavArgs): DeviceDetailsViewModel + } + private fun getIsE2EIEnabled() { viewModelScope.launch { isE2EIEnabledUseCase().let { 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..96be9eaef09 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAssetImporter.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.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 +import javax.inject.Inject + +interface ImportMediaAssetImporter { + suspend fun importAsset(uri: String): ImportedMediaAsset? +} + +class ImportMediaAssetImporterImpl @Inject constructor( + 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..8b70b796075 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,13 +32,11 @@ 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 @@ -65,7 +57,6 @@ 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 @@ -74,7 +65,7 @@ import javax.inject.Inject class ImportMediaAuthenticatedViewModel @Inject constructor( 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 +138,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 +160,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 +200,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/ImportMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt index d6c3e9511a1..fedf66a69fc 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 @@ -247,7 +247,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..b3b088da816 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingModule.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.sharing + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +interface ImportMediaSharingModule { + + @Binds + fun bindImportMediaAssetImporter(impl: ImportMediaAssetImporterImpl): ImportMediaAssetImporter +} 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..ecd78991ce4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarImageGateway.kt @@ -0,0 +1,77 @@ +/* + * 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 dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import okio.Path +import javax.inject.Inject + +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 @Inject constructor( + private val avatarImageManager: AvatarImageManager, + private val dispatchers: DispatcherProvider, + @ApplicationContext 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() +} + +@Module +@InstallIn(ViewModelComponent::class) +interface AvatarImageGatewayModule { + @Binds + fun bindAvatarImageGateway(gateway: AndroidAvatarImageGateway): AvatarImageGateway +} 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..7e1c2e1c8b1 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 @@ -82,12 +82,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 +207,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 +273,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..b32be531aa6 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 @@ -45,7 +37,6 @@ 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 @@ -60,11 +51,9 @@ class AvatarPickerViewModel @Inject constructor( 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 +67,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 +81,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 +95,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 +108,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 +144,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/qr/AndroidSelfQRCodeAssetRepository.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/AndroidSelfQRCodeAssetRepository.kt new file mode 100644 index 00000000000..3b77ef5c6da --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/AndroidSelfQRCodeAssetRepository.kt @@ -0,0 +1,53 @@ +/* + * 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 dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.withContext +import okio.Path.Companion.toPath +import java.io.FileOutputStream +import javax.inject.Inject + +class AndroidSelfQRCodeAssetRepository @Inject constructor( + @ApplicationContext 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/ui/userprofile/qr/SelfQRCodeAssetRepository.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeAssetRepository.kt new file mode 100644 index 00000000000..c83a2411fa2 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeAssetRepository.kt @@ -0,0 +1,28 @@ +/* + * 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 java.io.OutputStream + +interface SelfQRCodeAssetRepository { + suspend fun saveQRCode(qrCodeImage: SelfQRCodeImage): String +} + +fun interface SelfQRCodeImage { + fun writeTo(outputStream: OutputStream) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt new file mode 100644 index 00000000000..8dbcde60e86 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt @@ -0,0 +1,33 @@ +/* + * 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 dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +interface SelfQRCodeModule { + + @Binds + fun bindSelfQRCodeAssetRepository( + repository: AndroidSelfQRCodeAssetRepository + ): SelfQRCodeAssetRepository +} 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..01f7ac38020 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 @@ -61,6 +59,7 @@ 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,10 @@ import kotlinx.coroutines.launch @Composable fun SelfQRCodeScreen( navigator: Navigator, - viewModel: SelfQRCodeViewModel = hiltViewModel() + args: SelfQrCodeNavArgs, + viewModel: SelfQRCodeViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ) ) { if (viewModel.selfQRCodeState.hasError) { navigator.navigateBack() @@ -98,7 +100,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 +221,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 +285,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..ac0a292662d 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,30 @@ */ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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, +@HiltViewModel(assistedFactory = SelfQRCodeViewModel.Factory::class) +class SelfQRCodeViewModel @AssistedInject constructor( + @Assisted 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 +49,6 @@ class SelfQRCodeViewModel @Inject constructor( ) ) private set - private val cachePath: Path - get() = kaliumFileSystem.rootCachePath init { viewModelScope.launch { @@ -73,34 +56,18 @@ 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() + @AssistedFactory + interface Factory { + fun create(args: SelfQrCodeNavArgs): SelfQRCodeViewModel } + 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 +83,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/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/ServiceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModel.kt index 00940bb6f9a..46b02b1fb82 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 @@ -75,10 +73,11 @@ class ServiceDetailsViewModel @Inject constructor( private val removeMemberFromConversation: RemoveMemberFromConversationUseCase, private val addServiceToConversation: AddServiceToConversationUseCase, private val addMemberToConversation: AddMemberToConversationUseCase, - savedStateHandle: SavedStateHandle + serviceDetailsNavArgsProvider: ServiceDetailsNavArgsProvider ) : ViewModel() { - private val serviceDetailsNavArgs: ServiceDetailsNavArgs = savedStateHandle.navArgs() + private val serviceDetailsNavArgs: ServiceDetailsNavArgs = + serviceDetailsNavArgsProvider.serviceDetailsNavArgs() private val serviceId: ServiceId = serviceDetailsNavArgs.id.serviceId private val conversationId: QualifiedID? = serviceDetailsNavArgs.conversationId 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/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 7a3fb4fc1eb..84610074234 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,8 @@ 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 +1158,7 @@ class WireActivityViewModelTest { currentSessionFlow = { currentSessionFlow }, doesValidSessionExist = { doesValidSessionExist }, getServerConfigUseCase = { getServerConfigUseCase }, - deepLinkProcessor = { deepLinkProcessor }, - intentsProcessor = { intentsProcessor }, + intentGateway = { intentGateway }, observeSessions = { observeSessionsUseCase }, accountSwitch = { switchAccount }, servicesManager = { servicesManager }, @@ -1205,7 +1214,7 @@ class WireActivityViewModelTest { } fun withDeepLinkResult(result: DeepLinkResult): Arrangement { - coEvery { deepLinkProcessor(any(), any()) } returns result + coEvery { intentGateway.parseDeepLink(any()) } returns result return this } @@ -1303,7 +1312,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 +1349,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/login/sso/LoginSSOViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt index b56397c214c..3da6ac2654c 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 @@ -29,7 +29,6 @@ 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 @@ -204,6 +203,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" @@ -1076,7 +1118,6 @@ class LoginSSOViewModelTest { init { MockKAnnotations.init(this) - mockUri() every { savedStateHandle.get(any()) } returns null every { savedStateHandle.set(any(), any()) } returns Unit every { clientScopeProviderFactory.create(any()).clientScope } returns clientScope @@ -1179,28 +1220,40 @@ 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( + savedStateHandle = savedStateHandle, + 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/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/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/home/conversations/attachment/MessageAttachmentsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModelTest.kt index 195d3c34db4..2eedae15dec 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 @@ -20,11 +20,11 @@ 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 +34,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 +62,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 +74,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess(".hidden.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -83,7 +85,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess("bad/name.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -94,7 +96,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess("bad\\name.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -105,7 +107,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess("bad\"name.txt") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -116,7 +118,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 +129,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 +141,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 +153,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 +165,7 @@ class MessageAttachmentsViewModelTest { .withHandleUriAssetSuccess(".") .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Visible) } @@ -174,7 +176,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 +189,7 @@ class MessageAttachmentsViewModelTest { .withAddAttachmentSuccess() .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) viewModel.onReplaceFileNameAutomatically() coVerify(exactly = 1) { @@ -209,7 +211,7 @@ class MessageAttachmentsViewModelTest { .withAddAttachmentSuccess() .arrange() - viewModel.onFilesSelected(listOf(mockk())) + viewModel.onFilesSelected(listOf("uri")) viewModel.onReplaceFileNameAutomatically() assertTrue(viewModel.incompatibleFileNameDialogState is IncompatibleFileNameDialogState.Hidden) @@ -221,7 +223,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 +236,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 +249,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 +263,7 @@ class MessageAttachmentsViewModelTest { .withAddAttachmentSuccess() .arrange() - viewModel.onFilesSelected(listOf(mockk(), mockk())) + viewModel.onFilesSelected(listOf("uri-1", "uri-2")) viewModel.onReplaceFileNameAutomatically() viewModel.onReplaceFileNameAutomatically() @@ -274,7 +276,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 +290,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 +317,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,6 +355,13 @@ 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 @@ -333,7 +371,7 @@ class MessageAttachmentsViewModelTest { ).toSavedStateHandle() @MockK - lateinit var handleUriAsset: HandleUriAssetUseCase + lateinit var assetImporter: MessageAttachmentAssetImporter @MockK lateinit var observeAttachments: ObserveAttachmentDraftsUseCase @@ -351,7 +389,7 @@ class MessageAttachmentsViewModelTest { lateinit var uploadManager: CellUploadManager @MockK - lateinit var fileManager: FileManager + lateinit var fileGateway: MessageAttachmentFileGateway @MockK lateinit var getMediaMetadata: GetMediaMetadataUseCase @@ -368,6 +406,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 +418,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 +428,8 @@ class MessageAttachmentsViewModelTest { dataSize = 100L, fileName = name, assetType = AttachmentType.GENERIC_FILE, - ) + ), + assetSizeExceeded = null, ) } } @@ -395,17 +439,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, + 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/composer/MessageComposerViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelArrangement.kt index d598fedad15..54dc62172fc 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,9 @@ 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 @@ -38,7 +35,6 @@ 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,7 +83,6 @@ internal class MessageComposerViewModelArrangement { init { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) - mockUri() every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId = conversationId) // Default empty values @@ -95,8 +90,6 @@ internal class MessageComposerViewModelArrangement { 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))) @@ -144,9 +137,6 @@ internal class MessageComposerViewModelArrangement { @MockK lateinit var sendTypingEvent: SendTypingEventUseCase - @MockK - lateinit var fileManager: FileManager - @MockK lateinit var currentSessionFlowUseCase: CurrentSessionFlowUseCase @@ -156,7 +146,7 @@ internal class MessageComposerViewModelArrangement { @MockK lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase - private val fakeKaliumFileSystem = FakeKaliumFileSystem() + val tempWritableAttachmentUriProvider = FakeTempWritableAttachmentUriProvider() private val viewModel by lazy { MessageComposerViewModel( @@ -171,8 +161,7 @@ internal class MessageComposerViewModelArrangement { enqueueMessageSelfDeletion = enqueueMessageSelfDeletionUseCase, persistNewSelfDeletingStatus = persistSelfDeletionStatus, sendTypingEvent = sendTypingEvent, - kaliumFileSystem = fakeKaliumFileSystem, - fileManager = fileManager, + tempWritableAttachmentUriProvider = tempWritableAttachmentUriProvider, currentSessionFlowUseCase = currentSessionFlowUseCase, observeEstablishedCalls = observeEstablishedCalls, globalDataStore = globalDataStore, @@ -198,6 +187,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/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..b9211dad10d --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModelTest.kt @@ -0,0 +1,129 @@ +/* + * 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 androidx.lifecycle.SavedStateHandle +import com.ramcosta.composedestinations.generated.app.navargs.toSavedStateHandle +import com.wire.android.config.CoroutineTestExtension +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.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import okio.Path.Companion.toPath +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(CoroutineTestExtension::class) +class ImagesPreviewViewModelTest { + + @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(listOf("content://asset/first", "content://asset/second"), arrangement.assetImporter.importedUris) + 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(listOf("content://asset/clean", "content://asset/broken"), arrangement.assetImporter.importedUris) + 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(uri: String, importedMediaAsset: ImportedMediaAsset?) = apply { + assetImporter.assets[uri] = importedMediaAsset + } + + fun arrange( + assetUris: ArrayList = arrayListOf(), + ): Pair = this to ImagesPreviewViewModel( + savedStateHandle = savedStateHandle(assetUris), + assetImporter = assetImporter, + ) + + private fun savedStateHandle(assetUris: ArrayList): SavedStateHandle = + ImagesPreviewNavArgs( + conversationId = ConversationId("conversation-value", "conversation-domain"), + conversationName = "Conversation", + assetUriList = ArrayList(assetUris.map { it.toUri() }) + ).toSavedStateHandle() + } + + private class FakeImagesPreviewAssetImporter : ImagesPreviewAssetImporter { + 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/home/conversations/messages/ConversationMessagesViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt index e6118290df9..42b54d60b21 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 @@ -32,7 +32,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 @@ -95,7 +94,7 @@ class ConversationMessagesViewModelArrangement { lateinit var observeConversationDetails: ObserveConversationDetailsUseCase @MockK - lateinit var fileManager: FileManager + lateinit var assetFileGateway: ConversationAssetFileGateway @MockK lateinit var getMessageAsset: GetMessageAssetUseCase @@ -138,7 +137,7 @@ class ConversationMessagesViewModelArrangement { getMessageById, updateAssetMessageDownloadStatus, observeAssetStatuses, - fileManager, + assetFileGateway, TestDispatcherProvider(), getMessagesForConversationUseCase, fetchOlderNomadMessagesByConversationUseCase, @@ -190,7 +189,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 +257,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/messagecomposer/recordaudio/RecordAudioViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelTest.kt index 9d9350da14a..bc7f487661c 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 + private 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/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..fd4d4163764 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, @@ -576,14 +548,6 @@ class BackupAndRestoreViewModelTest { } 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 +557,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 +586,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/NewLoginViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelTest.kt index e783b6a2da2..8ef9d372ec5 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,10 @@ 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 @@ -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( @@ -663,17 +691,12 @@ class NewLoginViewModelTest { every { savedStateHandle[any()] = any() } returns Unit - every { - savedStateHandle.navArgs() - } returns LoginNavArgs() 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 +820,7 @@ class NewLoginViewModelTest { } fun withNomadAutoLogin(nomadServiceUrl: String) = apply { - every { - savedStateHandle.navArgs() - } returns LoginNavArgs( + loginNavArgs = LoginNavArgs( ssoCodeAutoLogin = SSOCodeAutoLogin( ssoCode = "wire-sso-code", nomadServiceUrl = nomadServiceUrl, @@ -812,9 +833,7 @@ class NewLoginViewModelTest { every { savedStateHandle.get(any()) } returns null - every { - savedStateHandle.navArgs() - } returns LoginNavArgs() + loginNavArgs = LoginNavArgs() } fun withUserIdentifierAlreadySet(userIdentifier: String) = apply { @@ -824,9 +843,7 @@ class NewLoginViewModelTest { } 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 +861,7 @@ class NewLoginViewModelTest { } fun withCustomServerConfigDeepLink() = apply { - every { - savedStateHandle.navArgs() - } returns LoginNavArgs( + loginNavArgs = LoginNavArgs( loginPasswordPath = LoginPasswordPath( customServerConfig = ServerConfig.STAGING ) @@ -879,18 +894,21 @@ class NewLoginViewModelTest { } private var defaultSSOCodeConfig: String = String.EMPTY + private var loginNavArgs: LoginNavArgs = LoginNavArgs() fun arrange() = this to NewLoginViewModel( validateEmailOrSSOCodeUseCase, coreLogic, savedStateHandle, + loginNavArgs, clientScopeProviderFactory, userDataStoreProvider, loginViewModelExtension, loginSSOViewModelExtension, dispatchers, ServerConfig.STAGING, - defaultSSOCodeConfig + defaultSSOCodeConfig, + NewLoginRecoverableLogoutExceptionDetector() ) } 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..c3d880eace6 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 @@ -204,6 +200,19 @@ class DebugDataOptionsViewModelTest { assertEquals("7", viewModel.state.currentApiVersion) } + @Test + fun `given debug data info is available, view state should contain debug id and commitish`() = runTest { + val (_, viewModel) = DebugDataOptionsHiltArrangement() + .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 @@ -291,8 +300,7 @@ class DebugDataOptionsViewModelTest { internal class DebugDataOptionsHiltArrangement { - @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..dde3cc6e3e4 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,6 +73,91 @@ class ImportMediaAuthenticatedViewModelTest { } } + @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, + ) + inner class Arrangement { @MockK @@ -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/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..66a24e862af 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 @@ -622,7 +620,7 @@ class ServiceDetailsViewModelTest { lateinit var addMemberToConversation: AddMemberToConversationUseCase @MockK - lateinit var savedStateHandle: SavedStateHandle + lateinit var serviceDetailsNavArgsProvider: ServiceDetailsNavArgsProvider private val selfUser = TestUser.SELF_USER @@ -640,7 +638,7 @@ class ServiceDetailsViewModelTest { removeMemberFromConversation, addServiceToConversation, addMemberToConversation, - savedStateHandle + serviceDetailsNavArgsProvider ) } @@ -659,14 +657,14 @@ class ServiceDetailsViewModelTest { } fun withServiceBot(service: BotService, conversationId: ConversationId? = CONVERSATION_ID) = apply { - every { savedStateHandle.navArgs() } returns ServiceDetailsNavArgs( + every { serviceDetailsNavArgsProvider.serviceDetailsNavArgs() } returns ServiceDetailsNavArgs( conversationId, ServiceDetailsNavArgs.Id.BotServiceId(service) ) } fun withServiceApp(service: UserId, conversationId: ConversationId? = CONVERSATION_ID) = apply { - every { savedStateHandle.navArgs() } returns ServiceDetailsNavArgs( + every { serviceDetailsNavArgsProvider.serviceDetailsNavArgs() } returns ServiceDetailsNavArgs( conversationId, ServiceDetailsNavArgs.Id.AppId(service) ) 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..0549d2a5fb6 --- /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 okio.Path.Companion.toPath +import javax.inject.Inject + +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/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/CellFileExternalActionsModule.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActionsModule.kt new file mode 100644 index 00000000000..e7c30e69c4e --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActionsModule.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 + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface CellFileExternalActionsModule { + @Binds + fun bindCellFileExternalActions( + androidCellFileExternalActions: AndroidCellFileExternalActions, + ): CellFileExternalActions +} 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..a19ffb233f6 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 @@ -40,7 +40,6 @@ 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.kalium.cells.data.FileFilters import com.wire.kalium.cells.data.SortingSpec @@ -73,7 +72,6 @@ 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") @@ -84,7 +82,7 @@ class CellViewModel @Inject constructor( 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, @@ -310,7 +308,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 +320,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 +378,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/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..3514b54c3a2 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 @@ -26,6 +26,8 @@ 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 @@ -53,7 +55,6 @@ 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 @@ -90,7 +91,6 @@ class CellViewModelTest { modifiedTime = 1234567890L, ) ) - val localFilePath = "localPath".toPath() } private val dispatcher = UnconfinedTestDispatcher() @@ -125,7 +125,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 +141,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 +164,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 +198,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 @@ -267,6 +287,9 @@ class CellViewModelTest { @MockK lateinit var isCellAvailableUseCase: IsAtLeastOneCellAvailableUseCase + @MockK + lateinit var fileExternalActions: CellFileExternalActions + @MockK lateinit var fileHelper: FileHelper @@ -347,6 +370,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("") @@ -367,7 +406,7 @@ class CellViewModelTest { deleteCellAsset = deleteCellAssetUseCase, restoreNodeFromRecycleBinUseCase = restoreNodeFromRecycleBinUseCase, isCellAvailable = isCellAvailableUseCase, - fileHelper = fileHelper, + fileExternalActions = fileExternalActions, onlineEditor = onlineEditor, getEditorUrl = getEditorUrlUseCase, cellFileActionsMenu = cellFileActionsMenu, 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 } From 66d08bc00e3aed71698390dfcb37201a485adea3 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 13:12:38 +0200 Subject: [PATCH 02/46] refactor: remove SavedStateHandle nav args from ViewModels --- .../wire/android/navigation/MainNavHost.kt | 23 +++- .../create/code/CreateAccountCodeScreen.kt | 6 +- .../create/code/CreateAccountCodeViewModel.kt | 19 ++-- .../details/CreateAccountDetailsScreen.kt | 6 +- .../details/CreateAccountDetailsViewModel.kt | 19 ++-- .../create/email/CreateAccountEmailScreen.kt | 6 +- .../email/CreateAccountEmailViewModel.kt | 19 ++-- .../CreateAccountOverviewViewModel.kt | 18 ++-- .../CreatePersonalAccountOverviewScreen.kt | 12 ++- .../summary/CreateAccountSummaryScreen.kt | 6 +- .../summary/CreateAccountSummaryViewModel.kt | 18 ++-- .../ui/authentication/login/LoginScreen.kt | 10 +- .../ui/authentication/login/LoginViewModel.kt | 25 +---- .../email/LoginEmailVerificationCodeScreen.kt | 3 +- .../login/email/LoginEmailViewModel.kt | 20 ++-- .../login/sso/LoginSSOScreen.kt | 5 +- .../login/sso/LoginSSOViewModel.kt | 102 ++++++++++++++---- .../authentication/welcome/WelcomeScreen.kt | 5 +- .../welcome/WelcomeViewModel.kt | 18 ++-- .../conversation/DebugConversationScreen.kt | 6 +- .../DebugConversationViewModel.kt | 19 ++-- .../com/wire/android/ui/home/HomeScreen.kt | 10 +- .../com/wire/android/ui/home/HomeViewModel.kt | 2 - .../CompositeMessageViewModel.kt | 6 +- .../home/conversations/ConversationScreen.kt | 46 ++++++-- .../attachment/MessageAttachmentsViewModel.kt | 18 ++-- .../banner/ConversationBannerViewModel.kt | 18 ++-- .../call/ConversationCallViewModel.kt | 18 ++-- .../composer/MessageComposerViewModel.kt | 18 ++-- .../details/GroupConversationDetailsScreen.kt | 6 +- .../GroupConversationDetailsViewModel.kt | 24 +++-- .../editguestaccess/EditGuestAccessScreen.kt | 19 +++- .../EditGuestAccessViewModel.kt | 18 ++-- .../CreatePasswordGuestLinkViewModel.kt | 20 ++-- .../CreatePasswordProtectedGuestLinkScreen.kt | 6 +- .../EditSelfDeletingMessagesScreen.kt | 14 ++- .../EditSelfDeletingMessagesViewModel.kt | 20 ++-- .../EditConversationMetadataViewModel.kt | 18 ++-- .../metadata/EditConversationNameScreen.kt | 6 +- .../GroupConversationAllParticipantsScreen.kt | 5 +- .../GroupConversationParticipantsViewModel.kt | 20 ++-- .../UpdateAppsAccessScreen.kt | 6 +- .../UpdateAppsAccessViewModel.kt | 18 ++-- .../ChannelAccessOnUpdateScreen.kt | 6 +- .../UpdateChannelAccessViewModel.kt | 19 ++-- .../info/ConversationInfoViewModel.kt | 19 ++-- .../ConversationAssetMessagesViewModel.kt | 21 ++-- .../media/ConversationMediaScreen.kt | 12 ++- .../media/preview/ImagesPreviewScreen.kt | 6 +- .../media/preview/ImagesPreviewViewModel.kt | 18 ++-- .../messagedetails/MessageDetailsScreen.kt | 5 +- .../messagedetails/MessageDetailsViewModel.kt | 18 ++-- .../messages/ConversationMessagesViewModel.kt | 18 ++-- .../messages/draft/MessageDraftViewModel.kt | 18 ++-- .../messages/item/MessageContentAndStatus.kt | 3 + .../ConversationMigrationViewModel.kt | 18 ++-- .../model/CompositeMessageArgs.kt | 4 +- .../home/conversations/model/MessageTypes.kt | 6 +- .../search/SearchUserViewModel.kt | 26 +++-- .../search/SearchUsersAndAppsScreen.kt | 7 +- .../AddMembersSearchScreen.kt | 8 +- .../AddMembersToConversationViewModel.kt | 21 ++-- .../SearchConversationMessagesScreen.kt | 6 +- .../SearchConversationMessagesViewModel.kt | 21 ++-- .../sendmessage/SendMessageViewModel.kt | 18 ++-- .../ui/home/drawer/HomeDrawerViewModel.kt | 2 - .../ui/home/gallery/MediaGalleryScreen.kt | 5 +- .../ui/home/gallery/MediaGalleryViewModel.kt | 19 ++-- .../email/verifyEmail/VerifyEmailScreen.kt | 5 +- .../email/verifyEmail/VerifyEmailViewModel.kt | 18 ++-- .../login/NewLoginNavArgsProvider.kt | 9 +- .../newauthentication/login/NewLoginScreen.kt | 4 +- .../login/NewLoginViewModel.kt | 19 ++-- .../login/password/NewLoginPasswordScreen.kt | 4 +- .../CreateAccountVerificationCodeScreen.kt | 6 +- .../CreateAccountVerificationCodeViewModel.kt | 19 ++-- .../details/CreateAccountDataDetailScreen.kt | 6 +- .../CreateAccountDataDetailViewModel.kt | 19 ++-- .../selector/CreateAccountSelectorScreen.kt | 6 +- .../CreateAccountSelectorViewModel.kt | 18 ++-- .../e2ei/E2eiCertificateDetailsScreen.kt | 6 +- .../e2ei/E2eiCertificateDetailsViewModel.kt | 19 ++-- .../other/OtherUserProfileScreen.kt | 5 +- .../other/OtherUserProfileScreenViewModel.kt | 18 ++-- .../ui/authentication/LoginViewModelTest.kt | 12 +-- .../CreateAccountDetailsViewModelTest.kt | 18 ++-- .../email/CreateAccountEmailViewModelTest.kt | 16 +-- .../login/email/LoginEmailViewModelTest.kt | 5 +- .../login/sso/LoginSSOViewModelTest.kt | 6 +- .../welcome/WelcomeViewModelTest.kt | 16 +-- .../DebugConversationViewModelTest.kt | 13 +-- .../wire/android/ui/home/HomeViewModelTest.kt | 5 - .../CompositeMessageViewModelTest.kt | 13 +-- .../MessageAttachmentsViewModelTest.kt | 10 +- .../banner/ConversationBannerViewModelTest.kt | 11 +- .../call/ConversationCallViewModelTest.kt | 12 +-- .../MessageComposerViewModelArrangement.kt | 8 +- .../details/GroupDetailsViewModelTest.kt | 18 +--- .../CreatePasswordGuestLinkViewModelTest.kt | 20 +--- .../EditGuestAccessViewModelTest.kt | 26 ++--- .../EditSelfDeletingMessagesViewModelTest.kt | 16 +-- .../GroupParticipantsViewModelTest.kt | 11 +- .../UpdateAppsAccessViewModelTest.kt | 32 +++--- .../UpdateChannelAccessViewModelTest.kt | 21 +--- .../ConversationInfoViewModelArrangement.kt | 8 +- .../preview/ImagesPreviewViewModelTest.kt | 8 +- ...onversationMessagesViewModelArrangement.kt | 8 +- ...SearchConversationMessagesViewModelTest.kt | 23 +--- .../draft/MessageDraftViewModelTest.kt | 14 +-- .../ConversationMigrationViewModelTest.kt | 11 +- .../search/SearchUserViewModelTest.kt | 26 ++--- .../AddMembersToConversationViewModelTest.kt | 15 +-- .../SendMessageViewModelArrangement.kt | 14 +-- .../ui/home/drawer/HomeDrawerViewModelTest.kt | 5 - .../home/gallery/MediaGalleryViewModelTest.kt | 28 ++--- .../account/email/VerifyEmailViewModelTest.kt | 15 +-- .../login/NewLoginViewModelTest.kt | 2 +- .../CreateAccountDataDetailViewModelTest.kt | 10 +- .../CreateAccountSelectorViewModelTest.kt | 16 +-- .../OtherUserProfileScreenViewModelTest.kt | 4 +- .../OtherUserProfileViewModelArrangement.kt | 21 ++-- .../navigation/wrapper/TabletDialogWrapper.kt | 9 +- .../feature/cells/ui/AllFilesScreen.kt | 4 +- .../android/feature/cells/ui/CellViewModel.kt | 33 +++--- .../cells/ui/ConversationFilesScreen.kt | 5 +- ...rsationFilesWithSlideInTransitionScreen.kt | 4 +- .../cells/ui/create/file/CreateFileScreen.kt | 6 +- .../ui/create/file/CreateFileViewModel.kt | 19 ++-- .../ui/create/folder/CreateFolderScreen.kt | 6 +- .../ui/create/folder/CreateFolderViewModel.kt | 19 ++-- .../ui/movetofolder/MoveToFolderScreen.kt | 10 +- .../ui/movetofolder/MoveToFolderViewModel.kt | 19 ++-- .../cells/ui/publiclink/PublicLinkScreen.kt | 5 +- .../ui/publiclink/PublicLinkViewModel.kt | 19 ++-- .../expiration/PublicLinkExpirationScreen.kt | 6 +- .../PublicLinkExpirationScreenViewModel.kt | 19 ++-- .../password/PublicLinkPasswordScreen.kt | 6 +- .../PublicLinkPasswordScreenViewModel.kt | 19 ++-- .../cells/ui/recyclebin/RecycleBinScreen.kt | 5 +- .../cells/ui/rename/RenameNodeScreen.kt | 8 +- .../cells/ui/rename/RenameNodeViewModel.kt | 19 ++-- .../feature/cells/ui/search/SearchScreen.kt | 5 +- .../cells/ui/search/SearchScreenViewModel.kt | 19 ++-- .../cells/ui/tags/AddRemoveTagsScreen.kt | 6 +- .../cells/ui/tags/AddRemoveTagsViewModel.kt | 18 ++-- .../ui/versioning/VersionHistoryScreen.kt | 6 +- .../ui/versioning/VersionHistoryViewModel.kt | 20 ++-- .../feature/cells/ui/CellViewModelTest.kt | 22 +--- .../ui/create/file/CreateFileViewModelTest.kt | 26 +++-- .../folder/CreateFolderViewModelTest.kt | 13 +-- .../movetofolder/MoveToFolderViewModelTest.kt | 18 ++-- .../ui/publiclink/PublicLinkViewModelTest.kt | 22 ++-- ...PublicLinkExpirationScreenViewModelTest.kt | 11 +- .../PublicLinkPasswordScreenViewModelTest.kt | 15 +-- .../ui/rename/RenameNodeViewModelTest.kt | 29 ++--- .../ui/search/SearchScreenViewModelTest.kt | 47 +++----- .../ui/tags/AddRemoveTagsViewModelTest.kt | 15 ++- .../versioning/VersionHistoryViewModelTest.kt | 27 +---- 158 files changed, 1184 insertions(+), 1098 deletions(-) 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..d5b3af1c99d 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt @@ -47,6 +47,7 @@ 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.feature.sketch.model.DrawingCanvasNavBackArgs import com.wire.android.navigation.transition.LocalSharedTransitionScope @@ -94,7 +95,12 @@ fun MainNavHost( val loginPasswordEntry = remember(navBackStackEntry) { navController.getBackStackEntry(NewLoginPasswordScreenDestination.route) } - dependency(hiltViewModel(loginPasswordEntry)) + val args = NewLoginPasswordScreenDestination.argsFrom(loginPasswordEntry.arguments) + dependency( + hiltViewModel(loginPasswordEntry) { factory -> + factory.create(args) + } + ) } // 👇 To reuse CellViewModel from the parent screen on SearchScreen @@ -102,7 +108,18 @@ fun MainNavHost( val parentEntry = remember(navBackStackEntry) { navController.previousBackStackEntry } - dependency(hiltViewModel(parentEntry ?: navBackStackEntry)) + dependency( + hiltViewModel( + parentEntry ?: navBackStackEntry, + creationCallback = { factory -> + val searchArgs = SearchScreenDestination.argsFrom(navBackStackEntry) + factory.create( + CellFilesNavArgs(conversationId = searchArgs.conversationId), + searchArgs + ) + } + ) + ) } // 👇 To tie TeamMigrationViewModel to PersonalToTeamMigrationNavGraph, @@ -120,8 +137,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/ui/authentication/create/code/CreateAccountCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeScreen.kt index 00b5fcc26a9..bc4d2cc6512 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 @@ -75,7 +75,11 @@ import kotlinx.coroutines.job @Composable fun CreateAccountCodeScreen( navigator: Navigator, - createAccountCodeViewModel: CreateAccountCodeViewModel = hiltViewModel() + args: CreateAccountNavArgs, + createAccountCodeViewModel: CreateAccountCodeViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..4f14d0604e4 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,17 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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, +@HiltViewModel(assistedFactory = CreateAccountCodeViewModel.Factory::class) +class CreateAccountCodeViewModel @AssistedInject constructor( + @Assisted val createAccountNavArgs: CreateAccountNavArgs, @KaliumCoreLogic private val coreLogic: CoreLogic, private val addAuthenticatedUser: AddAuthenticatedUserUseCase, private val clientScopeProviderFactory: ClientScopeProvider.Factory, @@ -64,8 +64,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() @@ -81,6 +79,11 @@ class CreateAccountCodeViewModel @Inject constructor( } } + @AssistedFactory + interface Factory { + fun create(args: CreateAccountNavArgs): CreateAccountCodeViewModel + } + fun resendCode() { codeState = codeState.copy(loading = true) viewModelScope.launch { 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..0b27e973b4a 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 @@ -74,7 +74,11 @@ import com.wire.kalium.logic.configuration.server.ServerConfig @Composable fun CreateAccountDetailsScreen( navigator: Navigator, - createAccountDetailsViewModel: CreateAccountDetailsViewModel = hiltViewModel() + args: CreateAccountNavArgs, + createAccountDetailsViewModel: CreateAccountDetailsViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..c8156b8df1c 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,28 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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, +@HiltViewModel(assistedFactory = CreateAccountDetailsViewModel.Factory::class) +class CreateAccountDetailsViewModel @AssistedInject constructor( + @Assisted 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() @@ -74,6 +72,11 @@ class CreateAccountDetailsViewModel @Inject constructor( } } + @AssistedFactory + interface Factory { + fun create(args: CreateAccountNavArgs): CreateAccountDetailsViewModel + } + fun onDetailsContinue() { detailsState = detailsState.copy(loading = true, continueEnabled = false) viewModelScope.launch { 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..27369f2079d 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 @@ -85,7 +85,11 @@ import com.wire.kalium.logic.configuration.server.ServerConfig @Composable fun CreateAccountEmailScreen( navigator: Navigator, - createAccountEmailViewModel: CreateAccountEmailViewModel = hiltViewModel() + args: CreateAccountNavArgs, + createAccountEmailViewModel: CreateAccountEmailViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..eca0258ecf6 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,32 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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, +@HiltViewModel(assistedFactory = CreateAccountEmailViewModel.Factory::class) +class CreateAccountEmailViewModel @AssistedInject constructor( + @Assisted 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 @@ -68,6 +66,11 @@ class CreateAccountEmailViewModel @Inject constructor( } } + @AssistedFactory + interface Factory { + fun create(args: CreateAccountNavArgs): CreateAccountEmailViewModel + } + fun onEmailContinue() { emailState = emailState.copy(loading = true, continueEnabled = false) viewModelScope.launch { 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..97b0eae1139 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,23 @@ */ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -@HiltViewModel -class CreateAccountOverviewViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = CreateAccountOverviewViewModel.Factory::class) +class CreateAccountOverviewViewModel @AssistedInject constructor( + @Assisted val navArgs: CreateAccountOverviewNavArgs, defaultServerConfig: ServerConfig.Links ) : ViewModel() { - val navArgs: CreateAccountOverviewNavArgs = savedStateHandle.navArgs() val serverConfig: ServerConfig.Links = navArgs.customServerConfig ?: defaultServerConfig fun learnMoreUrl(): String = serverConfig.pricing + + @AssistedFactory + interface Factory { + fun create(args: CreateAccountOverviewNavArgs): CreateAccountOverviewViewModel + } } 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..97ddf3d8c63 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 @@ -64,7 +64,11 @@ import com.wire.kalium.logic.configuration.server.ServerConfig @Composable fun CreatePersonalAccountOverviewScreen( navigator: Navigator, - viewModel: CreateAccountOverviewViewModel = hiltViewModel() + args: CreateAccountOverviewNavArgs, + viewModel: CreateAccountOverviewViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ) ) { CreateAccountOverviewScreen(navigator, CreateAccountFlowType.CreatePersonalAccount, viewModel) } @@ -73,7 +77,11 @@ fun CreatePersonalAccountOverviewScreen( @Composable fun CreateTeamAccountOverviewScreen( navigator: Navigator, - viewModel: CreateAccountOverviewViewModel = hiltViewModel() + args: CreateAccountOverviewNavArgs, + viewModel: CreateAccountOverviewViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..8262758c679 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 @@ -52,7 +52,11 @@ import com.wire.android.ui.theme.wireTypography @Composable fun CreateAccountSummaryScreen( navigator: Navigator, - viewModel: CreateAccountSummaryViewModel = hiltViewModel() + args: CreateAccountSummaryNavArgs, + viewModel: CreateAccountSummaryViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..62c19412dbc 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,21 +21,25 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -@HiltViewModel -class CreateAccountSummaryViewModel @Inject constructor( - savedStateHandle: SavedStateHandle +@HiltViewModel(assistedFactory = CreateAccountSummaryViewModel.Factory::class) +class CreateAccountSummaryViewModel @AssistedInject constructor( + @Assisted createAccountSummaryNavArgs: CreateAccountSummaryNavArgs ) : ViewModel() { - private val createAccountSummaryNavArgs: CreateAccountSummaryNavArgs = savedStateHandle.navArgs() private val type: CreateAccountFlowType = createAccountSummaryNavArgs.type var summaryState: CreateAccountSummaryViewState by mutableStateOf(CreateAccountSummaryViewState(type)) private set + + @AssistedFactory + interface Factory { + fun create(args: CreateAccountSummaryNavArgs): CreateAccountSummaryViewModel + } } 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..08f35a9b830 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 @@ -86,7 +86,9 @@ import kotlinx.coroutines.launch fun LoginScreen( navigator: Navigator, loginNavArgs: LoginNavArgs, - loginEmailViewModel: LoginEmailViewModel = hiltViewModel() + loginEmailViewModel: LoginEmailViewModel = hiltViewModel( + creationCallback = { factory -> factory.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,6 +271,7 @@ private fun PreviewLoginScreen() = WireTheme { onBackPressed = {}, onSuccess = { _, _ -> }, onRemoveDeviceNeeded = {}, + loginNavArgs = LoginNavArgs(), loginEmailViewModel = hiltViewModel(), 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/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..4e6f3c12bff 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 @@ -34,12 +34,12 @@ import com.wire.android.di.KaliumCoreLogic import com.wire.android.ui.authentication.login.LoginNavArgs 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,6 +58,9 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -68,11 +71,11 @@ 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( +@HiltViewModel(assistedFactory = LoginEmailViewModel.Factory::class) +class LoginEmailViewModel @AssistedInject constructor( + @Assisted val loginNavArgs: LoginNavArgs, private val addAuthenticatedUser: AddAuthenticatedUserUseCase, clientScopeProviderFactory: ClientScopeProvider.Factory, private val savedStateHandle: SavedStateHandle, @@ -83,13 +86,13 @@ class LoginEmailViewModel @Inject constructor( defaultServerConfig: ServerConfig.Links, @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean, ) : 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() @@ -105,6 +108,11 @@ class LoginEmailViewModel @Inject constructor( @VisibleForTesting internal val loginJobData = MutableStateFlow(null) + @AssistedFactory + interface Factory { + fun create(args: LoginNavArgs): LoginEmailViewModel + } + init { userIdentifierTextState.setTextAndPlaceCursorAtEnd( if (preFilledUserIdentifier is PreFilledUserIdentifierType.PreFilled) { 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 72810476ad0..6efdf2012fd 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 @@ -63,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 = hiltViewModel( + creationCallback = { factory -> factory.create(loginNavArgs) } + ), scrollState: ScrollState = rememberScrollState() ) { val scope = rememberCoroutineScope() 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 4db41279435..660b3d137ee 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 @@ -32,8 +32,10 @@ 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.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 @@ -55,6 +57,9 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableSharedFlow @@ -62,33 +67,49 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -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 sessionExceptionClassifier: LoginSSOSessionExceptionClassifier, - private val dispatchers: DispatcherProvider, -) : LoginViewModel( - savedStateHandle, - clientScopeProviderFactory, - userDataStoreProvider, - coreLogic, - serverConfig -) { +@HiltViewModel(assistedFactory = LoginSSOViewModel.Factory::class) +class LoginSSOViewModel : LoginViewModel { + private val savedStateHandle: SavedStateHandle + 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 - @Inject constructor( + loginNavArgs: LoginNavArgs, + savedStateHandle: SavedStateHandle, + addAuthenticatedUser: AddAuthenticatedUserUseCase, + validateEmailUseCase: ValidateEmailUseCase, + coreLogic: CoreLogic, + clientScopeProviderFactory: ClientScopeProvider.Factory, + userDataStoreProvider: UserDataStoreProvider, + serverConfig: ServerConfig.Links, + ssoExtension: LoginSSOViewModelExtension, + sessionExceptionClassifier: LoginSSOSessionExceptionClassifier, + dispatchers: DispatcherProvider, + ) : this( + loginNavArgs, + savedStateHandle, + addAuthenticatedUser, + validateEmailUseCase, + coreLogic, + clientScopeProviderFactory, + userDataStoreProvider, + ssoExtension, + serverConfig, + sessionExceptionClassifier, + dispatchers, + ) + + @AssistedInject + constructor( + @Assisted loginNavArgs: LoginNavArgs, savedStateHandle: SavedStateHandle, addAuthenticatedUser: AddAuthenticatedUserUseCase, validateEmailUseCase: ValidateEmailUseCase, @@ -100,24 +121,59 @@ class LoginSSOViewModel( sessionExceptionClassifier: LoginSSOSessionExceptionClassifier, dispatchers: DispatcherProvider, ) : this( + loginNavArgs, savedStateHandle, addAuthenticatedUser, validateEmailUseCase, coreLogic, clientScopeProviderFactory, userDataStoreProvider, - LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic, defaultWebSocketEnabledByDefault), serverConfig, + LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic, defaultWebSocketEnabledByDefault), sessionExceptionClassifier, dispatchers, ) + private constructor( + loginNavArgs: LoginNavArgs, + savedStateHandle: SavedStateHandle, + 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.savedStateHandle = savedStateHandle + 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 { + @AssistedFactory + interface Factory { + fun create(args: LoginNavArgs): LoginSSOViewModel + } + + private fun observeSSOCodeInput() { ssoTextState.setTextAndPlaceCursorAtEnd(savedStateHandle[SSO_CODE_SAVED_STATE_KEY] ?: String.EMPTY) viewModelScope.launch { ssoTextState.textAsFlow().distinctUntilChanged().collectLatest { 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..a331e2f36d5 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 @@ -113,7 +113,10 @@ import kotlinx.coroutines.flow.scan @Composable fun WelcomeScreen( navigator: Navigator, - viewModel: WelcomeViewModel = hiltViewModel() + args: WelcomeNavArgs, + viewModel: WelcomeViewModel = hiltViewModel( + creationCallback = { factory -> factory.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..0c1e7ca8527 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,27 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class WelcomeViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = WelcomeViewModel.Factory::class) +class WelcomeViewModel @AssistedInject constructor( + @Assisted 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 @@ -51,6 +50,11 @@ class WelcomeViewModel @Inject constructor( checkNumberOfSessions() } + @AssistedFactory + interface Factory { + fun create(args: WelcomeNavArgs): WelcomeViewModel + } + private fun checkNumberOfSessions() { viewModelScope.launch { if (doesValidNomadAccountExist()) { 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..273e4063383 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 @@ -65,8 +65,12 @@ import com.wire.kalium.logic.data.id.ConversationId @Composable fun DebugConversationScreen( navigator: Navigator, + args: DebugConversationScreenNavArgs, modifier: Modifier = Modifier, - viewModel: DebugConversationViewModel = hiltViewModel(), + viewModel: DebugConversationViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..c7d74c59e38 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,25 @@ 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.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.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class DebugConversationViewModel @Inject constructor( +@HiltViewModel(assistedFactory = DebugConversationViewModel.Factory::class) +class DebugConversationViewModel @AssistedInject constructor( private val conversationDetails: ObserveConversationDetailsUseCase, private val resetMLSConversation: ResetMLSConversationUseCase, private val fetchConversation: FetchConversationUseCase, private val feedConversation: DebugFeedConversationUseCase, private val getConversationEpochFromCC: GetConversationEpochFromCCUseCase, - savedStateHandle: SavedStateHandle, + @Assisted args: DebugConversationScreenNavArgs, ) : ActionsViewModel() { - val args: DebugConversationScreenNavArgs = savedStateHandle.navArgs() - val conversationId = args.conversationId private val _state = MutableStateFlow(DebugConversationViewState()) @@ -62,6 +60,11 @@ class DebugConversationViewModel @Inject constructor( loadConversationDetails() } + @AssistedFactory + interface Factory { + fun create(args: DebugConversationScreenNavArgs): DebugConversationViewModel + } + private fun loadConversationDetails() = viewModelScope.launch { conversationDetails(conversationId) .collect { result -> 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..5a2fd601278 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 @@ -72,6 +72,7 @@ import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDes 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.CellFilesNavArgs import com.wire.android.feature.cells.ui.CellViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient @@ -159,7 +160,7 @@ fun HomeScreen( } } - LaunchedEffect(homeViewModel.savedStateHandle) { + LaunchedEffect(Unit) { showNotificationsFlow.launch() } @@ -376,7 +377,12 @@ fun HomeContent( homeStateHolder.navigator.navController .getBackStackEntry(HomeScreenDestination.route) } - dependency(hiltViewModel(parentEntry)) + dependency( + hiltViewModel( + parentEntry, + creationCallback = { factory -> factory.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..bf50dab6bc3 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 @@ -50,7 +49,6 @@ import javax.inject.Inject @Suppress("LongParameterList") @HiltViewModel class HomeViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, private val dataStore: UserDataStore, private val observeSelf: ObserveSelfUserUseCase, private val needsToRegisterClient: NeedsToRegisterClientUseCase, 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..8f2c2694dfe 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,10 +21,8 @@ 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 @@ -47,12 +45,10 @@ interface CompositeMessageViewModel { @HiltViewModel(assistedFactory = CompositeMessageViewModelImpl.Factory::class) class CompositeMessageViewModelImpl @AssistedInject constructor( private val sendButtonActionMessageUseCase: SendButtonActionMessageUseCase, - savedStateHandle: SavedStateHandle, @Assisted 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) 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 03ab64570da..d96de78c11c 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 @@ -248,21 +248,49 @@ 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 = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), + conversationBannerViewModel: ConversationBannerViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), + conversationCallViewModel: ConversationCallViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), + conversationMessagesViewModel: ConversationMessagesViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), + messageComposerViewModel: MessageComposerViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), + sendMessageViewModel: SendMessageViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), + conversationMigrationViewModel: ConversationMigrationViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), + messageDraftViewModel: MessageDraftViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), + messageAttachmentsViewModel: MessageAttachmentsViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), ) { val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current 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 f82f4141645..e1d300aef87 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 @@ -21,7 +21,6 @@ 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 @@ -30,7 +29,6 @@ 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.ramcosta.composedestinations.generated.app.navArgs import com.wire.android.util.GetMediaMetadataUseCase import com.wire.kalium.cells.domain.CellUploadEvent import com.wire.kalium.cells.domain.CellUploadManager @@ -43,6 +41,9 @@ 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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -51,12 +52,11 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel -class MessageAttachmentsViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = MessageAttachmentsViewModel.Factory::class) +class MessageAttachmentsViewModel @AssistedInject constructor( + @Assisted conversationNavArgs: ConversationNavArgs, private val assetImporter: MessageAttachmentAssetImporter, private val observeAttachments: ObserveAttachmentDraftsUseCase, private val addAttachment: AddAttachmentDraftUseCase, @@ -68,7 +68,6 @@ class MessageAttachmentsViewModel @Inject constructor( 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()) @@ -278,6 +277,11 @@ class MessageAttachmentsViewModel @Inject constructor( } } } + + @AssistedFactory + interface Factory { + fun create(args: ConversationNavArgs): MessageAttachmentsViewModel + } } sealed interface FailedAttachmentDialogState { 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..62e87ebe5ad 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,19 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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, +@HiltViewModel(assistedFactory = ConversationBannerViewModel.Factory::class) +class ConversationBannerViewModel @AssistedInject constructor( + @Assisted private val conversationNavArgs: ConversationNavArgs, private val observeConversationMembersByTypes: ObserveConversationMembersByTypesUseCase, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val notifyConversationIsOpen: NotifyConversationIsOpenUseCase, @@ -56,7 +56,6 @@ class ConversationBannerViewModel @Inject constructor( var bannerState by mutableStateOf(null) - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId init { @@ -76,6 +75,11 @@ class ConversationBannerViewModel @Inject constructor( } } + @AssistedFactory + interface Factory { + fun create(args: ConversationNavArgs): ConversationBannerViewModel + } + @Suppress("ComplexMethod") private fun handleConversationMemberTypes(userTypesInfo: Set) { val containsService = userTypesInfo.any { it.isAppOrBot() } 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..f2b4fb61f71 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,11 @@ 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,6 +47,9 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -57,12 +58,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 +@HiltViewModel(assistedFactory = ConversationCallViewModel.Factory::class) @Suppress("LongParameterList", "TooManyFunctions") -class ConversationCallViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class ConversationCallViewModel @AssistedInject constructor( + @Assisted private val conversationNavArgs: ConversationNavArgs, @CurrentAccount val currentAccount: UserId, private val observeOngoingCalls: ObserveOngoingCallsUseCase, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, @@ -78,7 +78,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()) @@ -97,6 +96,11 @@ class ConversationCallViewModel @Inject constructor( observeCallingActivatedEvent() } + @AssistedFactory + interface Factory { + fun create(args: ConversationNavArgs): ConversationCallViewModel + } + private fun observeCallingActivatedEvent() { viewModelScope.launch { observeConferenceCallingEnabled() 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 a1e735815a0..95403d22183 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 @@ -21,7 +21,6 @@ package com.wire.android.ui.home.conversations.composer 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 @@ -31,7 +30,6 @@ 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.dispatchers.DispatcherProvider import com.wire.kalium.logic.configuration.FileSharingStatus @@ -52,6 +50,9 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.collectLatest @@ -63,12 +64,11 @@ 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, +@HiltViewModel(assistedFactory = MessageComposerViewModel.Factory::class) +class MessageComposerViewModel @AssistedInject constructor( + @Assisted conversationNavArgs: ConversationNavArgs, private val dispatchers: DispatcherProvider, private val isFileSharingEnabled: IsFileSharingEnabledUseCase, private val observeConversationInteractionAvailability: ObserveConversationInteractionAvailabilityUseCase, @@ -94,7 +94,6 @@ class MessageComposerViewModel @Inject constructor( var tempWritableImageUri: String? = null private set - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId var visitLinkDialogState: VisitLinkDialogState by mutableStateOf( @@ -246,4 +245,9 @@ class MessageComposerViewModel @Inject constructor( messageComposerViewState.value = messageComposerViewState.value.copy(isCallOngoing = hasOngoingCalls) } } + + @AssistedFactory + interface Factory { + fun create(args: ConversationNavArgs): MessageComposerViewModel + } } 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..61e19a04a1e 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 @@ -147,7 +147,11 @@ fun GroupConversationDetailsScreen( editChannelAccessResultRecipient: ResultRecipient, conversationFoldersScreenResultRecipient: ResultRecipient, - viewModel: GroupConversationDetailsViewModel = hiltViewModel(), + args: GroupConversationDetailsNavArgs, + viewModel: GroupConversationDetailsViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), ) { val scope = rememberCoroutineScope() val resources = LocalContext.current.resources 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..f673cce3a22 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 @@ -50,6 +48,9 @@ import com.wire.kalium.logic.feature.publicuser.RefreshUsersWithoutMetadataUseCa 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.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.MutableStateFlow @@ -63,11 +64,11 @@ 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( +@HiltViewModel(assistedFactory = GroupConversationDetailsViewModel.Factory::class) +class GroupConversationDetailsViewModel @AssistedInject constructor( + @Assisted private val groupConversationDetailsNavArgs: GroupConversationDetailsNavArgs, private val dispatcher: DispatcherProvider, private val observeConversationDetails: ObserveConversationDetailsUseCase, observeConversationMembers: ObserveParticipantsForConversationUseCase, @@ -75,14 +76,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)) @@ -95,6 +98,11 @@ class GroupConversationDetailsViewModel @Inject constructor( observeConversationDetails() } + @AssistedFactory + interface Factory { + fun create(args: GroupConversationDetailsNavArgs): GroupConversationDetailsViewModel + } + private suspend fun groupDetailsFlow(): Flow = observeConversationDetails(conversationId) .filterIsInstance() .map { it.conversationDetails } 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..3f9ef68338b 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 @@ -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,12 @@ import com.wire.android.util.shareViaIntent @Composable fun EditGuestAccessScreen( navigator: Navigator, + args: EditGuestAccessNavArgs, modifier: Modifier = Modifier, - editGuestAccessViewModel: EditGuestAccessViewModel = hiltViewModel() + editGuestAccessViewModel: EditGuestAccessViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ) ) { val scrollState = rememberScrollState() val snackbarHostState = LocalSnackbarHostState.current @@ -238,5 +243,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..d5c7f0ec5f0 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,6 +45,9 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -59,11 +60,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import javax.inject.Inject -@HiltViewModel +@HiltViewModel(assistedFactory = EditGuestAccessViewModel.Factory::class) @Suppress("LongParameterList", "TooManyFunctions") -class EditGuestAccessViewModel @Inject constructor( +class EditGuestAccessViewModel @AssistedInject constructor( + @Assisted private val editGuestAccessNavArgs: EditGuestAccessNavArgs, private val dispatcher: DispatcherProvider, private val updateConversationAccessRole: UpdateConversationAccessRoleUseCase, private val observeConversationDetails: ObserveConversationDetailsUseCase, @@ -76,10 +77,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 @@ -98,6 +97,11 @@ class EditGuestAccessViewModel @Inject constructor( checkIfUserCanCreatePasswordProtectedLinks() } + @AssistedFactory + interface Factory { + fun create(args: EditGuestAccessNavArgs): EditGuestAccessViewModel + } + private val syncCodeMutex = Mutex() private var isGuestCodeUpdated = false 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..5597b590af7 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,37 +22,41 @@ 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.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.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CreatePasswordGuestLinkViewModel @Inject constructor( +@HiltViewModel(assistedFactory = CreatePasswordGuestLinkViewModel.Factory::class) +class CreatePasswordGuestLinkViewModel @AssistedInject constructor( + @Assisted private val 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 + @AssistedFactory + interface Factory { + fun create(args: CreatePasswordGuestLinkNavArgs): CreatePasswordGuestLinkViewModel + } + suspend fun observePasswordValidation() { combine( state.passwordTextState.textAsFlow(), 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..bd546df005a 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 @@ -73,7 +73,11 @@ import kotlinx.coroutines.launch @Composable fun CreatePasswordProtectedGuestLinkScreen( navigator: Navigator, - viewModel: CreatePasswordGuestLinkViewModel = hiltViewModel(), + args: CreatePasswordGuestLinkNavArgs, + viewModel: CreatePasswordGuestLinkViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..b73ad014048 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 @@ -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,11 @@ import com.wire.android.util.ui.sectionWithElements @Composable fun EditSelfDeletingMessagesScreen( navigator: Navigator, - editSelfDeletingMessagesViewModel: EditSelfDeletingMessagesViewModel = hiltViewModel(), + args: EditSelfDeletingMessagesNavArgs, + editSelfDeletingMessagesViewModel: EditSelfDeletingMessagesViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), ) { val scrollState = rememberScrollState() val context = LocalContext.current @@ -169,7 +174,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..abcd0d02ae0 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,28 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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 +@HiltViewModel(assistedFactory = EditSelfDeletingMessagesViewModel.Factory::class) @Suppress("LongParameterList", "TooManyFunctions") -class EditSelfDeletingMessagesViewModel @Inject constructor( +class EditSelfDeletingMessagesViewModel @AssistedInject constructor( + @Assisted 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( @@ -68,6 +67,11 @@ class EditSelfDeletingMessagesViewModel @Inject constructor( observeSelfDeletionTimerSettingsForConversation() } + @AssistedFactory + interface Factory { + fun create(args: EditSelfDeletingMessagesNavArgs): EditSelfDeletingMessagesViewModel + } + private fun observeSelfDeletionTimerSettingsForConversation() { viewModelScope.launch { // TODO(refactor): Move all this logic to a UseCase 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..84ecd4edc8d 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,6 +35,9 @@ 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.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.dropWhile @@ -45,17 +46,15 @@ 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( +@HiltViewModel(assistedFactory = EditConversationMetadataViewModel.Factory::class) +class EditConversationMetadataViewModel @AssistedInject constructor( + @Assisted 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() @@ -67,6 +66,11 @@ class EditConversationMetadataViewModel @Inject constructor( observeConversationNameChanges() } + @AssistedFactory + interface Factory { + fun create(args: EditConversationNameNavArgs): EditConversationMetadataViewModel + } + private fun getConversationDetails() { viewModelScope.launch { val conversationDetails = observeConversationDetails(conversationId) 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..a3feffc73a0 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 @@ -40,7 +40,11 @@ import com.wire.android.util.ui.PreviewMultipleThemes fun EditConversationNameScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, - viewModel: EditConversationMetadataViewModel = hiltViewModel(), + args: EditConversationNameNavArgs, + viewModel: EditConversationMetadataViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), ) { with(viewModel) { LaunchedEffect(editConversationState.completed) { 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..10954eff14d 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 @@ -57,7 +57,10 @@ import com.wire.android.ui.userprofile.service.ServiceDetailsNavArgs fun GroupConversationAllParticipantsScreen( navigator: Navigator, navArgs: GroupConversationAllParticipantsNavArgs, - viewModel: GroupConversationParticipantsViewModel = hiltViewModel() + viewModel: GroupConversationParticipantsViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..eadc5b39ab6 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,20 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -open class GroupConversationParticipantsViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = GroupConversationParticipantsViewModel.Factory::class) +open class GroupConversationParticipantsViewModel @AssistedInject constructor( + @Assisted private val conversationId: QualifiedID, private val observeConversationMembers: ObserveParticipantsForConversationUseCase, private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, ) : ViewModel() { @@ -43,14 +43,16 @@ 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() } + @AssistedFactory + interface Factory { + fun create(conversationId: QualifiedID): GroupConversationParticipantsViewModel + } + private fun runRefreshUsersWithoutMetadata() { viewModelScope.launch { 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..58521674832 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 @@ -54,7 +54,11 @@ import kotlinx.coroutines.launch @Composable fun UpdateAppsAccessScreen( navigator: Navigator, - updateAppsAccessViewModel: UpdateAppsAccessViewModel = hiltViewModel() + args: UpdateAppsAccessNavArgs, + updateAppsAccessViewModel: UpdateAppsAccessViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..3dabcd61058 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,6 +37,9 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -49,20 +50,18 @@ 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( +@HiltViewModel(assistedFactory = UpdateAppsAccessViewModel.Factory::class) +class UpdateAppsAccessViewModel @AssistedInject constructor( + @Assisted 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 @@ -80,6 +79,11 @@ class UpdateAppsAccessViewModel @Inject constructor( observeConversationDetails() } + @AssistedFactory + interface Factory { + fun create(args: UpdateAppsAccessNavArgs): UpdateAppsAccessViewModel + } + @Suppress("DestructuringDeclarationWithTooManyEntries") private fun observeConversationDetails() { viewModelScope.launch { 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..8492fb26df3 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 @@ -38,7 +38,11 @@ import com.wire.android.ui.home.newconversation.channelaccess.ChannelAccessScree @Composable fun ChannelAccessOnUpdateScreen( resultNavigator: ResultBackNavigator, - updateChannelAccessViewModel: UpdateChannelAccessViewModel = hiltViewModel() + args: UpdateChannelAccessArgs, + updateChannelAccessViewModel: UpdateChannelAccessViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..e7ab6678839 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,28 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class UpdateChannelAccessViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = UpdateChannelAccessViewModel.Factory::class) +class UpdateChannelAccessViewModel @AssistedInject constructor( + @Assisted 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 @@ -52,6 +50,11 @@ class UpdateChannelAccessViewModel @Inject constructor( val conversationId: String = channelAccessNavArgs.conversationId + @AssistedFactory + interface Factory { + fun create(args: UpdateChannelAccessArgs): UpdateChannelAccessViewModel + } + fun updateChannelAddPermission(newPermission: ChannelAddPermissionType) { viewModelScope.launch { val result = updateChannelAddPermission( 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..889d87350a9 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,7 +21,6 @@ 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 @@ -29,7 +28,6 @@ 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 +39,22 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class ConversationInfoViewModel @Inject constructor( +@HiltViewModel(assistedFactory = ConversationInfoViewModel.Factory::class) +class ConversationInfoViewModel @AssistedInject constructor( + @Assisted 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, ) : ViewModel() { - - private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId var conversationInfoViewState by mutableStateOf(ConversationInfoViewState(conversationId)) @@ -65,6 +63,11 @@ class ConversationInfoViewModel @Inject constructor( fetchMLSVerificationStatus() } + @AssistedFactory + interface Factory { + fun create(args: ConversationNavArgs): ConversationInfoViewModel + } + private fun fetchMLSVerificationStatus() { viewModelScope.launch { fetchConversationMLSVerificationStatus(conversationId) 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..658a2d5fbde 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,29 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel +@HiltViewModel(assistedFactory = ConversationAssetMessagesViewModel.Factory::class) @Suppress("LongParameterList", "TooManyFunctions") -class ConversationAssetMessagesViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class ConversationAssetMessagesViewModel @AssistedInject constructor( + @Assisted 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 @@ -56,6 +54,11 @@ class ConversationAssetMessagesViewModel @Inject constructor( observeAssetStatuses() } + @AssistedFactory + interface Factory { + fun create(args: ConversationMediaNavArgs): ConversationAssetMessagesViewModel + } + private fun loadAssets() = viewModelScope.launch { val assetsResult = getAssetMessages.invoke( conversationId = conversationId, 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 bb8209aee02..f941bcfa584 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 @@ -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,15 @@ import kotlinx.serialization.Serializable @Composable fun ConversationMediaScreen( navigator: Navigator, - conversationAssetMessagesViewModel: ConversationAssetMessagesViewModel = hiltViewModel(), - conversationMessagesViewModel: ConversationMessagesViewModel = hiltViewModel() + args: ConversationMediaNavArgs, + conversationAssetMessagesViewModel: ConversationAssetMessagesViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), + conversationMessagesViewModel: ConversationMessagesViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(ConversationNavArgs(args.conversationId)) } + ) ) { val permissionPermanentlyDeniedDialogState = rememberVisibilityState() val context = LocalContext.current 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..dba08e0f21b 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 @@ -89,7 +89,11 @@ import okio.Path.Companion.toPath fun ImagesPreviewScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, - imagesPreviewViewModel: ImagesPreviewViewModel = hiltViewModel(), + args: ImagesPreviewNavArgs, + imagesPreviewViewModel: ImagesPreviewViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ), checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = hiltViewModel() ) { LaunchedEffect(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 d74419afdca..fb8ecaed0ca 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 @@ -20,22 +20,21 @@ package com.wire.android.ui.home.conversations.media.preview 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 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.launch -import javax.inject.Inject -@HiltViewModel -class ImagesPreviewViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = ImagesPreviewViewModel.Factory::class) +class ImagesPreviewViewModel @AssistedInject constructor( + @Assisted private val navArgs: ImagesPreviewNavArgs, private val assetImporter: ImagesPreviewAssetImporter ) : ViewModel() { - private val navArgs: ImagesPreviewNavArgs = savedStateHandle.navArgs() var viewState by mutableStateOf( ImagesPreviewState( conversationId = navArgs.conversationId, @@ -48,6 +47,11 @@ class ImagesPreviewViewModel @Inject constructor( handleAssets() } + @AssistedFactory + interface Factory { + fun create(args: ImagesPreviewNavArgs): ImagesPreviewViewModel + } + fun onSelected(index: Int) { viewState = viewState.copy(selectedIndex = index) } 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..d8bd7587a9c 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 @@ -67,7 +67,10 @@ import kotlinx.coroutines.launch @Composable fun MessageDetailsScreen( navigator: Navigator, - viewModel: MessageDetailsViewModel = hiltViewModel() + args: MessageDetailsNavArgs, + viewModel: MessageDetailsViewModel = hiltViewModel( + creationCallback = { factory -> factory.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..3ec14ffb2cb 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,32 +21,36 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class MessageDetailsViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = MessageDetailsViewModel.Factory::class) +class MessageDetailsViewModel @AssistedInject constructor( + @Assisted 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 var messageDetailsState: MessageDetailsState by mutableStateOf(MessageDetailsState()) + @AssistedFactory + interface Factory { + fun create(args: MessageDetailsNavArgs): MessageDetailsViewModel + } + init { viewModelScope.launch { messageDetailsState = messageDetailsState.copy( 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 4ab6c04cfd9..fd36104fb2c 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 @@ -21,10 +21,8 @@ package com.wire.android.ui.home.conversations.messages 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 @@ -65,6 +63,9 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.CoroutineScope @@ -81,14 +82,13 @@ 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 +@HiltViewModel(assistedFactory = ConversationMessagesViewModel.Factory::class) @Suppress("LongParameterList", "TooManyFunctions") -class ConversationMessagesViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +class ConversationMessagesViewModel @AssistedInject constructor( + @Assisted private val conversationNavArgs: ConversationNavArgs, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val getMessageAsset: GetMessageAssetUseCase, private val getMessageByIdUseCase: GetMessageByIdUseCase, @@ -108,7 +108,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 @@ -135,6 +134,11 @@ class ConversationMessagesViewModel @Inject constructor( observeAssetStatuses() } + @AssistedFactory + interface Factory { + fun create(conversationNavArgs: ConversationNavArgs): ConversationMessagesViewModel + } + val currentTimeInMillisFlow: Flow = flow { while (true) { delay(CURRENT_TIME_REFRESH_WINDOW_IN_MILLIS) 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..ed894a37c2a 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,25 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class MessageDraftViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = MessageDraftViewModel.Factory::class) +class MessageDraftViewModel @AssistedInject constructor( + @Assisted 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)) @@ -56,6 +55,11 @@ class MessageDraftViewModel @Inject constructor( loadMessageDraft() } + @AssistedFactory + interface Factory { + fun create(conversationNavArgs: ConversationNavArgs): MessageDraftViewModel + } + fun clearDraft() { viewModelScope.launch { if (state.value.quotedMessageId != null) { 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..785dab7e660 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 @@ -239,6 +239,7 @@ private fun MessageContent( VerticalSpace.x4() } MessageBody( + conversationId = message.conversationId, messageBody = messageContent.messageBody, searchQuery = searchQuery, isAvailable = !message.isPending && message.isAvailable, @@ -281,6 +282,7 @@ private fun MessageContent( VerticalSpace.x4() } MessageBody( + conversationId = message.conversationId, messageBody = messageContent.messageBody, isAvailable = !message.isPending && message.isAvailable, onOpenProfile = onOpenProfile, @@ -403,6 +405,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..7bc06e56b68 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,25 @@ 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.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.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class ConversationMigrationViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = ConversationMigrationViewModel.Factory::class) +class ConversationMigrationViewModel @AssistedInject constructor( + @Assisted private val conversationNavArgs: ConversationNavArgs, private val observeConversationDetails: ObserveConversationDetailsUseCase ) : ViewModel() { @@ -51,7 +51,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 +66,11 @@ class ConversationMigrationViewModel @Inject constructor( migratedConversationId = activeOneOnOneConversationId } } + } } + + @AssistedFactory + interface Factory { + fun create(args: ConversationNavArgs): ConversationMigrationViewModel } } 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..18c8ef2523b 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 @@ -88,6 +88,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 +96,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 +156,7 @@ internal fun MessageBody( buttonList?.also { VerticalSpace.x4() MessageButtonsContent( + conversationId = conversationId, messageId = messageId, buttonList = it, messageStyle = messageStyle @@ -163,6 +166,7 @@ internal fun MessageBody( @Composable fun MessageButtonsContent( + conversationId: ConversationId, messageId: String, buttonList: List, messageStyle: MessageStyle, @@ -174,7 +178,7 @@ fun MessageButtonsContent( CompositeMessageArgs, CompositeMessageViewModelImpl.Factory >( - CompositeMessageArgs(messageId) + CompositeMessageArgs(conversationId, messageId) ) ) { Column( 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..05eecfc8e89 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,6 +33,9 @@ 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.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.ImmutableSet @@ -48,26 +49,18 @@ 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( +@HiltViewModel(assistedFactory = SearchUserViewModel.Factory::class) +class SearchUserViewModel @AssistedInject constructor( + @Assisted 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)) @@ -161,6 +154,11 @@ class SearchUserViewModel @Inject constructor( excludingMembersOfConversation = addMembersSearchNavArgs?.conversationId, customDomain = domain ) + + @AssistedFactory + interface Factory { + fun create(addMembersSearchNavArgs: AddMembersSearchNavArgs?): SearchUserViewModel + } } data class SearchUserState( 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..6e038471cdb 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 @@ -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,10 @@ private fun SearchAllPeopleOrContactsScreen( actionType: ItemActionType, onOpenUserProfile: (Contact) -> Unit, onContactChecked: (Boolean, Contact) -> Unit, - searchUserViewModel: SearchUserViewModel = hiltViewModel(), + addMembersSearchNavArgs: AddMembersSearchNavArgs? = null, + searchUserViewModel: SearchUserViewModel = hiltViewModel( + creationCallback = { factory -> factory.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..6ae3b5b0fd6 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 @@ -42,7 +42,10 @@ import com.wire.kalium.logic.data.user.UserId fun AddMembersSearchScreen( navigator: Navigator, navArgs: AddMembersSearchNavArgs, - addMembersToConversationViewModel: AddMembersToConversationViewModel = hiltViewModel(), + addMembersToConversationViewModel: AddMembersToConversationViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(navArgs) } + ), ) { if (addMembersToConversationViewModel.newGroupState.isCompleted) { navigator.navigateBack() @@ -75,6 +78,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..5ffe17290f2 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,30 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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( +@HiltViewModel(assistedFactory = AddMembersToConversationViewModel.Factory::class) +class AddMembersToConversationViewModel @AssistedInject constructor( + @Assisted 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 @@ -75,6 +73,11 @@ class AddMembersToConversationViewModel @Inject constructor( ) } } + + @AssistedFactory + interface Factory { + fun create(args: AddMembersSearchNavArgs): AddMembersToConversationViewModel + } } data class AddMembersToConversationState( 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..82129a8eba8 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 @@ -59,7 +59,11 @@ import com.wire.android.ui.common.R as commonR @Composable fun SearchConversationMessagesScreen( navigator: Navigator, - searchConversationMessagesViewModel: SearchConversationMessagesViewModel = hiltViewModel() + navArgs: SearchConversationMessagesNavArgs, + searchConversationMessagesViewModel: SearchConversationMessagesViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..e9c6beb8ef4 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,29 @@ 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.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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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( +@HiltViewModel(assistedFactory = SearchConversationMessagesViewModel.Factory::class) +class SearchConversationMessagesViewModel @AssistedInject constructor( + @Assisted 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 @@ -79,4 +77,9 @@ class SearchConversationMessagesViewModel @Inject constructor( searchResult = messagesResultFlow, ) } + + @AssistedFactory + interface Factory { + fun create(args: SearchConversationMessagesNavArgs): SearchConversationMessagesViewModel + } } 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..81e98d9dc64 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,6 +73,9 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow @@ -82,12 +83,11 @@ 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, +@HiltViewModel(assistedFactory = SendMessageViewModel.Factory::class) +class SendMessageViewModel @AssistedInject constructor( + @Assisted private val conversationNavArgs: ConversationNavArgs, private val sendAssetMessage: ScheduleNewAssetMessageUseCase, private val sendTextMessage: SendTextMessageUseCase, private val sendMultipartMessage: SendMultipartMessageUseCase, @@ -112,7 +112,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() @@ -546,4 +545,9 @@ class SendMessageViewModel @Inject constructor( private companion object { const val MAX_LIMIT_MESSAGE_SEND = 20 } + + @AssistedFactory + interface Factory { + fun create(args: ConversationNavArgs): SendMessageViewModel + } } 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..0ce3219eda0 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 @@ -44,7 +43,6 @@ import javax.inject.Inject @Suppress("LongParameterList") @HiltViewModel class HomeDrawerViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, private val observeArchivedUnreadConversationsCount: Lazy, private val observeSelfUser: ObserveSelfUserUseCase, private val getTeamUrl: GetTeamUrlUseCase, 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..dbaf45ae97a 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 @@ -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 = hiltViewModel( + creationCallback = { factory -> factory.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..247e78d00d0 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,6 +40,9 @@ 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.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.asSharedFlow @@ -50,12 +51,11 @@ 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, +@HiltViewModel(assistedFactory = MediaGalleryViewModel.Factory::class) +class MediaGalleryViewModel @AssistedInject constructor( + @Assisted private val mediaGalleryNavArgs: MediaGalleryNavArgs, private val getConversationDetails: ObserveConversationDetailsUseCase, private val dispatchers: DispatcherProvider, private val getImageData: GetMessageAssetUseCase, @@ -65,8 +65,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 @@ -85,6 +83,11 @@ class MediaGalleryViewModel @Inject constructor( setupImageAsset() } + @AssistedFactory + interface Factory { + fun create(args: MediaGalleryNavArgs): MediaGalleryViewModel + } + private fun setupImageAsset() = viewModelScope.launch { if (cellAssetId == null) { mediaGalleryViewState = mediaGalleryViewState.copy( 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..6d3662d5b51 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 @@ -57,7 +57,10 @@ import com.wire.android.util.ui.stringWithStyledArgs @Composable fun VerifyEmailScreen( navigator: Navigator, - viewModel: VerifyEmailViewModel = hiltViewModel() + args: VerifyEmailNavArgs, + viewModel: VerifyEmailViewModel = hiltViewModel( + creationCallback = { factory -> factory.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..2a1f26e3475 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,27 +20,31 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class VerifyEmailViewModel @Inject constructor( +@HiltViewModel(assistedFactory = VerifyEmailViewModel.Factory::class) +class VerifyEmailViewModel @AssistedInject constructor( private val updateEmail: UpdateEmailUseCase, - savedStateHandle: SavedStateHandle + @Assisted private val verifyEmailNavArgs: VerifyEmailNavArgs ) : ViewModel() { var state: VerifyEmailState by mutableStateOf(VerifyEmailState()) private set - private val verifyEmailNavArgs: VerifyEmailNavArgs = savedStateHandle.navArgs() val newEmail: String = verifyEmailNavArgs.newEmail + @AssistedFactory + interface Factory { + fun create(args: VerifyEmailNavArgs): VerifyEmailViewModel + } + fun onResendVerificationEmailClicked() { newEmail.let { state = state.copy(isResendEmailEnabled = false) 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 index 78992dc6c22..8af07ab711c 100644 --- 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 @@ -18,13 +18,6 @@ package com.wire.android.ui.newauthentication.login -import androidx.lifecycle.SavedStateHandle -import com.ramcosta.composedestinations.generated.app.navArgs -import com.wire.android.ui.authentication.login.LoginNavArgs import javax.inject.Inject -class NewLoginNavArgsProvider @Inject constructor( - private val savedStateHandle: SavedStateHandle, -) { - fun loginNavArgs(): LoginNavArgs = savedStateHandle.navArgs() -} +class NewLoginNavArgsProvider @Inject constructor() 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..f2818fd8102 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 @@ -97,7 +97,9 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun NewLoginScreen( navigator: Navigator, navArgs: LoginNavArgs, - viewModel: NewLoginViewModel = hiltViewModel() + viewModel: NewLoginViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(navArgs) } + ) ) { val context = LocalContext.current val currentKeyboardController by rememberUpdatedState(LocalSoftwareKeyboardController.current) 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 238b7a9c428..c9370a4bdb6 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 @@ -60,6 +60,9 @@ 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.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 @@ -69,16 +72,15 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json -import javax.inject.Inject import javax.inject.Named @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel +@HiltViewModel(assistedFactory = NewLoginViewModel.Factory::class) class NewLoginViewModel( + private val loginNavArgs: LoginNavArgs, private val validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase, val coreLogic: CoreLogic, savedStateHandle: SavedStateHandle, - private val loginNavArgs: LoginNavArgs, val clientScopeProviderFactory: ClientScopeProvider.Factory, val userDataStoreProvider: UserDataStoreProvider, private val loginExtension: LoginViewModelExtension, @@ -89,12 +91,12 @@ class NewLoginViewModel( private val recoverableLogoutExceptionDetector: NewLoginRecoverableLogoutExceptionDetector, ) : ActionsViewModel() { - @Inject + @AssistedInject constructor( + @Assisted loginNavArgs: LoginNavArgs, validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase, @KaliumCoreLogic coreLogic: CoreLogic, savedStateHandle: SavedStateHandle, - loginNavArgsProvider: NewLoginNavArgsProvider, addAuthenticatedUser: AddAuthenticatedUserUseCase, clientScopeProviderFactory: ClientScopeProvider.Factory, userDataStoreProvider: UserDataStoreProvider, @@ -104,10 +106,10 @@ class NewLoginViewModel( @DefaultWebSocketEnabledByDefault defaultWebSocketEnabledByDefault: Boolean, recoverableLogoutExceptionDetector: NewLoginRecoverableLogoutExceptionDetector, ) : this( + loginNavArgs, validateEmailOrSSOCode, coreLogic, savedStateHandle, - loginNavArgsProvider.loginNavArgs(), clientScopeProviderFactory, userDataStoreProvider, LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), @@ -118,6 +120,11 @@ class NewLoginViewModel( recoverableLogoutExceptionDetector ) + @AssistedFactory + interface Factory { + fun create(args: LoginNavArgs): NewLoginViewModel + } + private val preFilledUserIdentifier: PreFilledUserIdentifierType = loginNavArgs.userHandle ?: PreFilledUserIdentifierType.None private var pendingNomadServiceUrl: String? = loginNavArgs.ssoCodeAutoLogin?.nomadServiceUrl private var pendingCookieLabel: String? = loginNavArgs.ssoCodeAutoLogin?.cookieLabel 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..590ef4daf33 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 @@ -105,7 +105,9 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun NewLoginPasswordScreen( navigator: Navigator, navArgs: LoginNavArgs, - loginEmailViewModel: LoginEmailViewModel = hiltViewModel() + loginEmailViewModel: LoginEmailViewModel = hiltViewModel( + creationCallback = { factory -> factory.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..61b313320ea 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 @@ -80,7 +80,11 @@ import kotlinx.coroutines.job @Composable fun CreateAccountVerificationCodeScreen( navigator: Navigator, - createAccountCodeVerificationViewModel: CreateAccountVerificationCodeViewModel = hiltViewModel() + args: CreateAccountDataNavArgs, + createAccountCodeVerificationViewModel: CreateAccountVerificationCodeViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..9a38ae82665 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,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.analytics.RegistrationAnalyticsManagerUseCase import com.wire.android.di.ClientScopeProvider @@ -46,14 +44,16 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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, +@HiltViewModel(assistedFactory = CreateAccountVerificationCodeViewModel.Factory::class) +class CreateAccountVerificationCodeViewModel @AssistedInject constructor( + @Assisted val createAccountNavArgs: CreateAccountDataNavArgs, @KaliumCoreLogic private val coreLogic: CoreLogic, private val addAuthenticatedUser: AddAuthenticatedUserUseCase, private val registrationAnalyticsManager: RegistrationAnalyticsManagerUseCase, @@ -62,8 +62,6 @@ class CreateAccountVerificationCodeViewModel @Inject constructor( @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean, ) : ViewModel() { - val createAccountNavArgs: CreateAccountDataNavArgs = savedStateHandle.navArgs() - val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig ?: defaultServerConfig val codeTextState: TextFieldState = TextFieldState() @@ -80,6 +78,11 @@ class CreateAccountVerificationCodeViewModel @Inject constructor( } } + @AssistedFactory + interface Factory { + fun create(args: CreateAccountDataNavArgs): CreateAccountVerificationCodeViewModel + } + fun resendCode() { codeState = codeState.copy(loading = true) viewModelScope.launch { 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..167aeb5eafa 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 @@ -95,7 +95,11 @@ import com.wire.kalium.logic.configuration.server.ServerConfig @Composable fun CreateAccountDataDetailScreen( navigator: Navigator, - createAccountDataDetailViewModel: CreateAccountDataDetailViewModel = hiltViewModel() + args: CreateAccountDataNavArgs, + createAccountDataDetailViewModel: CreateAccountDataDetailViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..2e3ea37fb8a 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,7 +21,6 @@ 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 @@ -30,23 +29,24 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject 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, +@HiltViewModel(assistedFactory = CreateAccountDataDetailViewModel.Factory::class) +class CreateAccountDataDetailViewModel @AssistedInject constructor( + @Assisted val createAccountNavArgs: CreateAccountDataNavArgs, private val validatePassword: ValidatePasswordUseCase, private val validateEmail: ValidateEmailUseCase, private val globalDataStore: GlobalDataStore, @@ -55,8 +55,6 @@ class CreateAccountDataDetailViewModel @Inject constructor( defaultServerConfig: ServerConfig.Links ) : ViewModel() { - val createAccountNavArgs: CreateAccountDataNavArgs = savedStateHandle.navArgs() - private var withPasswordTries = false val emailTextState: TextFieldState = TextFieldState(createAccountNavArgs.userRegistrationInfo.email) val nameTextState: TextFieldState = TextFieldState() @@ -87,6 +85,11 @@ class CreateAccountDataDetailViewModel @Inject constructor( } } + @AssistedFactory + interface Factory { + fun create(args: CreateAccountDataNavArgs): CreateAccountDataDetailViewModel + } + private fun onEmailContinue() { detailsState = detailsState.copy(loading = true, continueEnabled = false) viewModelScope.launch { 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..9bc10598e66 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 @@ -82,7 +82,11 @@ import com.wire.android.ui.common.R as commonR @Composable fun CreateAccountSelectorScreen( navigator: Navigator, - viewModel: CreateAccountSelectorViewModel = hiltViewModel() + args: CreateAccountSelectorNavArgs, + viewModel: CreateAccountSelectorViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..d1b441ef9f5 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,22 @@ */ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CreateAccountSelectorViewModel @Inject constructor( +@HiltViewModel(assistedFactory = CreateAccountSelectorViewModel.Factory::class) +class CreateAccountSelectorViewModel @AssistedInject constructor( + @Assisted 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 @@ -41,4 +40,9 @@ class CreateAccountSelectorViewModel @Inject constructor( fun onPageLoaded() = viewModelScope.launch { globalDataStore.setAnonymousRegistrationEnabled(false) } + + @AssistedFactory + interface Factory { + fun create(args: CreateAccountSelectorNavArgs): CreateAccountSelectorViewModel + } } 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..0c4abdaee10 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 @@ -61,7 +61,11 @@ import kotlinx.coroutines.withContext @Composable fun E2eiCertificateDetailsScreen( navigator: Navigator, - e2eiCertificateDetailsViewModel: E2eiCertificateDetailsViewModel = hiltViewModel() + args: E2eiCertificateDetailsScreenNavArgs, + e2eiCertificateDetailsViewModel: E2eiCertificateDetailsViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..dc71ffb5983 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,22 @@ */ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class E2eiCertificateDetailsViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = E2eiCertificateDetailsViewModel.Factory::class) +class E2eiCertificateDetailsViewModel @AssistedInject constructor( + @Assisted private val navArgs: E2eiCertificateDetailsScreenNavArgs, private val getSelfUser: GetSelfUserUseCase, ) : ViewModel() { - private val navArgs: E2eiCertificateDetailsScreenNavArgs = - savedStateHandle.navArgs() private var selfUserHandle: String? = null @@ -42,6 +40,11 @@ class E2eiCertificateDetailsViewModel @Inject constructor( getSelfUserHandle() } + @AssistedFactory + interface Factory { + fun create(args: E2eiCertificateDetailsScreenNavArgs): E2eiCertificateDetailsViewModel + } + private fun getSelfUserHandle() { viewModelScope.launch { selfUserHandle = getSelfUser()?.handle 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..2ebf4a45b6f 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 @@ -121,7 +121,10 @@ fun OtherUserProfileScreen( resultNavigator: ResultBackNavigator, conversationFoldersScreenResultRecipient: ResultRecipient, - viewModel: OtherUserProfileScreenViewModel = hiltViewModel() + viewModel: OtherUserProfileScreenViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..f574a80afc7 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,6 +51,9 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -63,11 +64,11 @@ 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( +@HiltViewModel(assistedFactory = OtherUserProfileScreenViewModel.Factory::class) +class OtherUserProfileScreenViewModel @AssistedInject constructor( + @Assisted private val otherUserProfileNavArgs: OtherUserProfileNavArgs, private val dispatchers: DispatcherProvider, private val observeUserInfo: ObserveUserInfoUseCase, private val userTypeMapper: UserTypeMapper, @@ -80,10 +81,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 @@ -105,6 +104,11 @@ class OtherUserProfileScreenViewModel @Inject constructor( getE2EIStatus() } + @AssistedFactory + interface Factory { + fun create(args: OtherUserProfileNavArgs): OtherUserProfileScreenViewModel + } + private fun getIfConversationExist() { viewModelScope.launch { val isOneToOneConversationCreated = isOneToOneConversationCreated(userId) 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/email/LoginEmailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt index 727e11dd517..3b6f07aa957 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 @@ -38,7 +38,6 @@ 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.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 @@ -861,9 +860,6 @@ class LoginEmailViewModelTest { 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,6 +873,7 @@ class LoginEmailViewModelTest { } fun arrange() = this to LoginEmailViewModel( + LoginNavArgs(loginPasswordPath = LoginPasswordPath(newServerConfig(1).links)), addAuthenticatedUserUseCase, clientScopeProviderFactory, savedStateHandle, 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 3da6ac2654c..bb0f703d44a 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 @@ -21,7 +21,6 @@ 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 @@ -1122,10 +1121,6 @@ class LoginSSOViewModelTest { every { savedStateHandle.set(any(), 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( @@ -1233,6 +1228,7 @@ class LoginSSOViewModelTest { fun arrange(): Pair { val viewModel = LoginSSOViewModel( + loginNavArgs = LoginNavArgs(loginPasswordPath = LoginPasswordPath(SERVER_CONFIG.links)), savedStateHandle = savedStateHandle, addAuthenticatedUser = addAuthenticatedUserUseCase, validateEmailUseCase = validateEmailUseCase, 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/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 2eedae15dec..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,8 +17,6 @@ */ 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 @@ -364,11 +362,9 @@ class MessageAttachmentsViewModelTest { 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 assetImporter: MessageAttachmentAssetImporter @@ -447,7 +443,7 @@ class MessageAttachmentsViewModelTest { fun arrange(): Pair { initializeMocks() val viewModel = MessageAttachmentsViewModel( - savedStateHandle = savedStateHandle, + conversationNavArgs = conversationNavArgs, assetImporter = assetImporter, observeAttachments = observeAttachments, addAttachment = addAttachment, 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 54dc62172fc..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,7 +18,6 @@ package com.wire.android.ui.home.conversations.composer -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.GlobalDataStore import com.wire.android.framework.TestConversation @@ -34,7 +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.ui.UIText import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.data.auth.AccountInfo @@ -83,7 +81,6 @@ internal class MessageComposerViewModelArrangement { init { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) - every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId = conversationId) // Default empty values coEvery { isFileSharingEnabledUseCase() } returns FileSharingStatus(FileSharingStatus.Value.EnabledAll, null) @@ -98,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 @@ -150,7 +146,7 @@ internal class MessageComposerViewModelArrangement { private val viewModel by lazy { MessageComposerViewModel( - savedStateHandle = savedStateHandle, + conversationNavArgs = conversationNavArgs, dispatchers = TestDispatcherProvider(), isFileSharingEnabled = isFileSharingEnabledUseCase, updateConversationReadDate = updateConversationReadDateUseCase, 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 index b9211dad10d..24fc14d20d0 100644 --- 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 @@ -18,8 +18,6 @@ package com.wire.android.ui.home.conversations.media.preview import androidx.core.net.toUri -import androidx.lifecycle.SavedStateHandle -import com.ramcosta.composedestinations.generated.app.navargs.toSavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.sharing.ImportedMediaAsset @@ -105,16 +103,16 @@ class ImagesPreviewViewModelTest { fun arrange( assetUris: ArrayList = arrayListOf(), ): Pair = this to ImagesPreviewViewModel( - savedStateHandle = savedStateHandle(assetUris), + navArgs = navArgs(assetUris), assetImporter = assetImporter, ) - private fun savedStateHandle(assetUris: ArrayList): SavedStateHandle = + private fun navArgs(assetUris: ArrayList): ImagesPreviewNavArgs = ImagesPreviewNavArgs( conversationId = ConversationId("conversation-value", "conversation-domain"), conversationName = "Conversation", assetUriList = ArrayList(assetUris.map { it.toUri() }) - ).toSavedStateHandle() + ) } private class FakeImagesPreviewAssetImporter : ImagesPreviewAssetImporter { 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 42b54d60b21..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 @@ -78,9 +76,6 @@ class ConversationMessagesViewModelArrangement { private val messagesChannel = Channel>(capacity = Channel.UNLIMITED) - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var getMessagesForConversationUseCase: GetMessagesForConversationUseCase @@ -131,7 +126,7 @@ class ConversationMessagesViewModelArrangement { private val viewModel: ConversationMessagesViewModel by lazy { ConversationMessagesViewModel( - savedStateHandle, + ConversationNavArgs(conversationId = conversationId), observeConversationDetails, getMessageAsset, getMessageById, @@ -156,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() 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/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/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/newauthentication/login/NewLoginViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelTest.kt index 8ef9d372ec5..2963330f370 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 @@ -897,10 +897,10 @@ class NewLoginViewModelTest { private var loginNavArgs: LoginNavArgs = LoginNavArgs() fun arrange() = this to NewLoginViewModel( + loginNavArgs, validateEmailOrSSOCodeUseCase, coreLogic, savedStateHandle, - loginNavArgs, clientScopeProviderFactory, userDataStoreProvider, loginViewModelExtension, 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/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/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/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..d611c54bd60 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 @@ -44,7 +44,9 @@ import com.wire.android.ui.common.topappbar.search.SearchTopBar fun AllFilesScreen( navigator: WireNavigator, modifier: Modifier = Modifier, - viewModel: CellViewModel = hiltViewModel(), + viewModel: CellViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(CellFilesNavArgs(), null) } + ), ) { val pagingListItems = viewModel.nodesFlow.collectAsLazyPagingItems() 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 a19ffb233f6..dca5bd7c023 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,7 +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.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 @@ -54,6 +53,9 @@ 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.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 @@ -72,12 +74,12 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel -class CellViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = CellViewModel.Factory::class) +class CellViewModel @AssistedInject constructor( + @Assisted private val navArgs: CellFilesNavArgs, + @Assisted private val searchNavArgs: SearchNavArgs?, private val getCellFilesPaged: GetPaginatedFilesFlowUseCase, private val deleteCellAsset: DeleteCellAssetUseCase, private val restoreNodeFromRecycleBinUseCase: RestoreNodeFromRecycleBinUseCase, @@ -89,15 +91,7 @@ class CellViewModel @Inject constructor( 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() @@ -468,6 +462,11 @@ class CellViewModel @Inject constructor( ) ) } + + @AssistedFactory + interface Factory { + fun create(navArgs: CellFilesNavArgs, searchNavArgs: SearchNavArgs?): CellViewModel + } } sealed interface CellViewIntent { 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..1392295ddf6 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 @@ -101,7 +101,10 @@ import kotlinx.coroutines.flow.flowOf fun ConversationFilesScreen( navigator: WireNavigator, animatedVisibilityScope: AnimatedVisibilityScope, - viewModel: CellViewModel = hiltViewModel(), + args: CellFilesNavArgs, + viewModel: CellViewModel = hiltViewModel( + creationCallback = { factory -> factory.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..771a84a7fa3 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 @@ -41,7 +41,9 @@ fun ConversationFilesWithSlideInTransitionScreen( navigator: WireNavigator, cellFilesNavArgs: CellFilesNavArgs, animatedVisibilityScope: AnimatedVisibilityScope, - viewModel: CellViewModel = hiltViewModel(), + viewModel: CellViewModel = hiltViewModel( + creationCallback = { factory -> factory.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/create/file/CreateFileScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreen.kt index 79197ca7df3..cf16a5e0cfa 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 @@ -63,8 +63,11 @@ import java.util.Locale fun CreateFileScreen( navigator: WireNavigator, resultNavigator: ResultBackNavigator, + args: CreateFileScreenNavArgs, modifier: Modifier = Modifier, - createFileViewModel: CreateFileViewModel = hiltViewModel() + createFileViewModel: CreateFileViewModel = hiltViewModel( + creationCallback = { factory -> factory.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..760bc49d821 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,22 @@ 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.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.map import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CreateFileViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = CreateFileViewModel.Factory::class) +class CreateFileViewModel @AssistedInject constructor( + @Assisted 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() @@ -68,6 +66,11 @@ class CreateFileViewModel @Inject constructor( } } + @AssistedFactory + interface Factory { + fun create(args: CreateFileScreenNavArgs): CreateFileViewModel + } + internal fun createFile(fileName: String) = viewModelScope.launch { viewState = viewState.copy(loading = true) 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..1d9e72ca172 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 @@ -68,8 +68,11 @@ import java.util.Locale fun CreateFolderScreen( navigator: WireNavigator, resultNavigator: ResultBackNavigator, + args: CreateFolderScreenNavArgs, modifier: Modifier = Modifier, - createFolderViewModel: CreateFolderViewModel = hiltViewModel() + createFolderViewModel: CreateFolderViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ) ) { val showErrorDialog = remember { mutableStateOf(false) } @@ -181,6 +184,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..5bf71a1e705 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,20 @@ 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.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.map import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class CreateFolderViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = CreateFolderViewModel.Factory::class) +class CreateFolderViewModel @AssistedInject constructor( + @Assisted 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()) @@ -62,6 +60,11 @@ class CreateFolderViewModel @Inject constructor( } } + @AssistedFactory + interface Factory { + fun create(args: CreateFolderScreenNavArgs): CreateFolderViewModel + } + internal fun createFolder(folderName: String) = viewModelScope.launch { viewState = viewState.copy(loading = true) 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..e099edaf9be 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 @@ -78,8 +78,11 @@ import com.wire.android.ui.theme.wireTypography fun MoveToFolderScreen( navigator: WireNavigator, createFolderResultRecipient: ResultRecipient, + args: MoveToFolderNavArgs, modifier: Modifier = Modifier, - moveToFolderViewModel: MoveToFolderViewModel = hiltViewModel() + moveToFolderViewModel: MoveToFolderViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(args) } + ) ) { val context = LocalContext.current val viewState by moveToFolderViewModel.state.collectAsState() @@ -292,6 +295,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..b883a5edf99 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,22 @@ 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.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.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class MoveToFolderViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = MoveToFolderViewModel.Factory::class) +class MoveToFolderViewModel @AssistedInject constructor( + @Assisted 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 @@ -65,6 +63,11 @@ class MoveToFolderViewModel @Inject constructor( loadFolders() } + @AssistedFactory + interface Factory { + fun create(args: MoveToFolderNavArgs): MoveToFolderViewModel + } + fun loadFolders() { viewModelScope.launch { getFoldersUseCase(currentPath) 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..6cfea64bc84 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 @@ -81,8 +81,11 @@ fun PublicLinkScreen( navigator: WireNavigator, onPasswordChange: ResultRecipient, onExpirationChange: ResultRecipient, + args: PublicLinkNavArgs, modifier: Modifier = Modifier, - viewModel: PublicLinkViewModel = hiltViewModel(), + viewModel: PublicLinkViewModel = hiltViewModel( + creationCallback = { factory -> factory.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..84ab1e8624d 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,24 @@ 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.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.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class PublicLinkViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = PublicLinkViewModel.Factory::class) +class PublicLinkViewModel @AssistedInject constructor( + @Assisted 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, @@ -227,6 +225,11 @@ class PublicLinkViewModel @Inject constructor( PublicLinkError.Remove -> onConfirmRemoval(true) } } + + @AssistedFactory + interface Factory { + fun create(navArgs: PublicLinkNavArgs): PublicLinkViewModel + } } internal data class PublicLinkViewState( 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..d819e8cb089 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 @@ -71,8 +71,12 @@ import kotlinx.parcelize.Parcelize @Composable internal fun PublicLinkExpirationScreen( resultNavigator: ResultBackNavigator, + args: PublicLinkExpirationScreenNavArgs, modifier: Modifier = Modifier, - viewModel: PublicLinkExpirationScreenViewModel = hiltViewModel(), + viewModel: PublicLinkExpirationScreenViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..70f1a4d11f2 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,6 +27,9 @@ 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.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.asStateFlow @@ -41,16 +42,13 @@ 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( +@HiltViewModel(assistedFactory = PublicLinkExpirationScreenViewModel.Factory::class) +internal class PublicLinkExpirationScreenViewModel @AssistedInject constructor( + @Assisted 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 @@ -196,6 +194,11 @@ internal class PublicLinkExpirationScreenViewModel @Inject constructor( private fun updateState(block: PublicLinkExpirationScreenViewState.() -> PublicLinkExpirationScreenViewState) { _state.update(block) } + + @AssistedFactory + interface Factory { + fun create(navArgs: PublicLinkExpirationScreenNavArgs): PublicLinkExpirationScreenViewModel + } } internal sealed class ExpirationError( 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..8ff93747c74 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 @@ -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 = + hiltViewModel( + creationCallback = { factory -> factory.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..c48a4e0dce4 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,25 @@ 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.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.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( +@HiltViewModel(assistedFactory = PublicLinkPasswordScreenViewModel.Factory::class) +internal class PublicLinkPasswordScreenViewModel @AssistedInject constructor( + @Assisted 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 @@ -182,6 +180,11 @@ internal class PublicLinkPasswordScreenViewModel @Inject constructor( _state.update(block) } + @AssistedFactory + interface Factory { + fun create(navArgs: PublicLinkPasswordNavArgs): PublicLinkPasswordScreenViewModel + } + private companion object { const val TYPING_DEBOUNCE_TIME = 200L } 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..1d5752bd78c 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 @@ -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 = hiltViewModel( + creationCallback = { factory -> factory.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..e7137fe0374 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 @@ -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 = hiltViewModel( + creationCallback = { factory -> factory.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..575551a8df0 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,23 @@ 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.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.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, +@HiltViewModel(assistedFactory = RenameNodeViewModel.Factory::class) +class RenameNodeViewModel @AssistedInject constructor( + @Assisted 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 @@ -73,6 +71,11 @@ class RenameNodeViewModel @Inject constructor( } } + @AssistedFactory + interface Factory { + fun create(args: RenameNodeNavArgs): RenameNodeViewModel + } + fun renameNode(newName: String) { viewState = viewState.copy(loading = true) viewModelScope.launch { 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..4f92ccb0b98 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 @@ -75,8 +75,11 @@ fun SearchScreen( navigator: WireNavigator, animatedVisibilityScope: AnimatedVisibilityScope, cellViewModel: CellViewModel, + args: SearchNavArgs, modifier: Modifier = Modifier, - searchScreenViewModel: SearchScreenViewModel = hiltViewModel(), + searchScreenViewModel: SearchScreenViewModel = hiltViewModel( + creationCallback = { factory -> factory.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..429a1d6aad3 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,6 +49,9 @@ 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.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.MutableStateFlow @@ -65,14 +66,13 @@ 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, +@HiltViewModel(assistedFactory = SearchScreenViewModel.Factory::class) +class SearchScreenViewModel @AssistedInject constructor( + @Assisted private val navArgs: SearchNavArgs, private val getAllTagsUseCase: GetAllTagsUseCase, private val getCellFilesPaged: GetPaginatedFilesFlowUseCase, private val getOwners: GetOwnersUseCase, @@ -90,8 +90,6 @@ class SearchScreenViewModel @Inject constructor( val conversationId: String?, ) - private val navArgs: SearchNavArgs = SearchScreenDestination.argsFrom(savedStateHandle) - val screenType = navArgs.screenType val parentRoute = navArgs.parentRoute @@ -374,6 +372,11 @@ class SearchScreenViewModel @Inject constructor( current.copy(sortingCriteria = criteria) } } + + @AssistedFactory + interface Factory { + fun create(navArgs: SearchNavArgs): SearchScreenViewModel + } } private fun CellConversation.toFilterConversationUi() = FilterConversationUi( 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..327fb15ba5b 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 @@ -77,8 +77,12 @@ import kotlinx.coroutines.launch @Composable fun AddRemoveTagsScreen( navigator: WireNavigator, + args: AddRemoveTagsNavArgs, modifier: Modifier = Modifier, - addRemoveTagsViewModel: AddRemoveTagsViewModel = hiltViewModel(), + addRemoveTagsViewModel: AddRemoveTagsViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..df0086be17b 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,15 +20,16 @@ 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.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.asStateFlow @@ -36,17 +37,15 @@ 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, +@HiltViewModel(assistedFactory = AddRemoveTagsViewModel.Factory::class) +class AddRemoveTagsViewModel @AssistedInject constructor( + @Assisted 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(",", ";", "/", "\\", "\"", "\'", "<", ">") @@ -66,6 +65,11 @@ class AddRemoveTagsViewModel @Inject constructor( } } + @AssistedFactory + interface Factory { + fun create(args: AddRemoveTagsNavArgs): AddRemoveTagsViewModel + } + fun isValidTag(): Boolean = with(tagsTextState) { disallowedChars.none { it in text } && text.length in ALLOWED_LENGTH } 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..e3370bc2fab 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 @@ -71,8 +71,12 @@ import kotlinx.coroutines.launch @Composable fun VersionHistoryScreen( navigator: WireNavigator, + args: VersionHistoryNavArgs, modifier: Modifier = Modifier, - versionHistoryViewModel: VersionHistoryViewModel = hiltViewModel() + versionHistoryViewModel: VersionHistoryViewModel = + hiltViewModel( + creationCallback = { factory -> factory.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..41d920b3bfc 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,6 +39,9 @@ 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.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -50,11 +51,10 @@ 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, +@HiltViewModel(assistedFactory = VersionHistoryViewModel.Factory::class) +class VersionHistoryViewModel @AssistedInject constructor( + @Assisted private val navArgs: VersionHistoryNavArgs, private val getNodeVersionsUseCase: GetNodeVersionsUseCase, private val fileSizeFormatter: FileSizeFormatter, private val restoreNodeVersionUseCase: RestoreNodeVersionUseCase, @@ -64,9 +64,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) @@ -273,4 +270,9 @@ class VersionHistoryViewModel @Inject constructor( const val DELAY_100_MS = 100L const val DELAY_500_MS = 500L } + + @AssistedFactory + interface Factory { + fun create(navArgs: VersionHistoryNavArgs): VersionHistoryViewModel + } } 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 3514b54c3a2..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,14 +17,11 @@ */ 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 @@ -46,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 @@ -60,10 +56,8 @@ 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 { @@ -267,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 @@ -314,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( @@ -401,7 +384,8 @@ class CellViewModelTest { ) return this to CellViewModel( - savedStateHandle = savedStateHandle, + navArgs = CellFilesNavArgs(conversationId = conversationId), + searchNavArgs = null, getCellFilesPaged = getCellFilesPagedUseCase, deleteCellAsset = deleteCellAssetUseCase, restoreNodeFromRecycleBinUseCase = restoreNodeFromRecycleBinUseCase, 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, From cda484bff8deac4db527ebb616489a294a7038cd Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 13:37:21 +0200 Subject: [PATCH 03/46] chore: prepare ViewModel creation boundary for Metro --- .../wire/android/di/AssistedViewModelExt.kt | 25 +++++++++ .../com/wire/android/di/ViewModelScoped.kt | 47 +++++++++++++++-- .../create/code/CreateAccountCodeScreen.kt | 4 +- .../details/CreateAccountDetailsScreen.kt | 4 +- .../create/email/CreateAccountEmailScreen.kt | 4 +- .../CreatePersonalAccountOverviewScreen.kt | 6 +-- .../summary/CreateAccountSummaryScreen.kt | 4 +- .../username/CreateAccountUsernameScreen.kt | 4 +- .../devices/register/RegisterDeviceScreen.kt | 6 +-- .../RegisterDeviceVerificationCodeScreen.kt | 4 +- .../devices/remove/RemoveDeviceScreen.kt | 6 +-- .../RemoveDeviceVerificationCodeScreen.kt | 4 +- .../login/LoginSavedInputStore.kt | 24 +++++++++ .../ui/authentication/login/LoginScreen.kt | 6 +-- .../login/SavedStateLoginSavedInputStore.kt | 52 +++++++++++++++++++ .../login/email/LoginEmailViewModel.kt | 9 ++-- .../login/sso/LoginSSOScreen.kt | 4 +- .../login/sso/LoginSSOViewModel.kt | 21 ++++---- .../authentication/welcome/WelcomeScreen.kt | 4 +- .../android/ui/common/AddContactButton.kt | 4 +- .../banner/SecurityClassificationBanner.kt | 6 +-- .../ConversationOptionsModalSheetLayout.kt | 4 +- .../ui/connection/ConnectionActionButton.kt | 4 +- .../wire/android/ui/debug/DebugDataOptions.kt | 4 +- .../com/wire/android/ui/debug/DebugScreen.kt | 4 +- .../conversations/UsersTypingIndicator.kt | 18 +++---- .../edit/MessageOptionsModalSheetLayout.kt | 4 +- .../conversations/messages/QuotedMessage.kt | 6 +-- .../home/conversations/model/MessageTypes.kt | 4 +- .../messagetypes/audio/AudioMessageType.kt | 6 +-- .../messagecomposer/MessageComposeActions.kt | 4 +- .../messagecomposer/MessageComposerInput.kt | 4 +- .../attachments/AdditionalOptionButton.kt | 4 +- .../recordaudio/RecordAudioComponent.kt | 4 +- .../JoinConversationViaDeepLinkDialog.kt | 4 +- .../code/NewLoginVerificationCodeScreen.kt | 2 +- .../newauthentication/login/NewLoginScreen.kt | 19 +++++-- .../login/NewLoginViewModel.kt | 29 +++-------- .../login/password/NewLoginPasswordScreen.kt | 4 +- .../CreateAccountVerificationCodeScreen.kt | 4 +- .../details/CreateAccountDataDetailScreen.kt | 4 +- .../selector/CreateAccountSelectorScreen.kt | 4 +- .../login/email/LoginEmailViewModelTest.kt | 10 ++-- .../login/sso/LoginSSOViewModelTest.kt | 10 ++-- .../login/NewLoginViewModelTest.kt | 22 +++----- .../feature/cells/ui/AllFilesScreen.kt | 3 +- .../feature/cells/ui/CellFilesScreen.kt | 11 ++++ .../cells/ui/ConversationFilesScreen.kt | 3 +- ...rsationFilesWithSlideInTransitionScreen.kt | 3 +- .../cells/ui/create/file/CreateFileScreen.kt | 4 +- .../ui/create/folder/CreateFolderScreen.kt | 9 ++-- .../ui/movetofolder/MoveToFolderScreen.kt | 9 ++-- .../cells/ui/publiclink/PublicLinkScreen.kt | 4 +- .../expiration/PublicLinkExpirationScreen.kt | 4 +- .../password/PublicLinkPasswordScreen.kt | 4 +- .../cells/ui/recyclebin/RecycleBinScreen.kt | 4 +- .../cells/ui/rename/RenameNodeScreen.kt | 4 +- .../feature/cells/ui/search/SearchScreen.kt | 9 ++-- .../cells/ui/tags/AddRemoveTagsScreen.kt | 4 +- .../ui/versioning/VersionHistoryScreen.kt | 4 +- 60 files changed, 321 insertions(+), 188 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginSavedInputStore.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/login/SavedStateLoginSavedInputStore.kt diff --git a/app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt b/app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt index f239779156e..63c165df87a 100644 --- a/app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt +++ b/app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt @@ -19,7 +19,11 @@ package com.wire.android.di import androidx.activity.ComponentActivity import androidx.activity.viewModels +import androidx.compose.runtime.Composable import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.hilt.navigation.compose.hiltViewModel import dagger.hilt.android.lifecycle.withCreationCallback inline fun ComponentActivity.assistedViewModels( @@ -27,3 +31,24 @@ inline fun ComponentActivity.assistedViewM ) = viewModels(extrasProducer = { defaultViewModelCreationExtras.withCreationCallback { factory -> create(factory) } }) + +@Composable +inline fun wireViewModel( + viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + }, + key: String? = null, +): VM = hiltViewModel(viewModelStoreOwner = viewModelStoreOwner, key = key) + +@Composable +inline fun wireViewModel( + viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + }, + key: String? = null, + noinline creationCallback: (VMF) -> VM +): VM = hiltViewModel( + viewModelStoreOwner = viewModelStoreOwner, + key = key, + creationCallback = creationCallback +) 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..9079bfe7a5e 100644 --- a/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt +++ b/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt @@ -32,7 +32,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 @@ -45,7 +45,7 @@ interface AssistedViewModelFactory { @Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") @Composable inline fun > - hiltViewModelScoped(arguments: R, clearDelay: Duration? = null): S where T : ViewModel, T : S = when { + 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 -> @@ -56,7 +56,7 @@ inline fun > - hiltViewModelScoped( + wireViewModelScoped( arguments: R, noinline keyInScopeResolver: KeyInScopeResolver, clearDelay: Duration? = null, @@ -78,7 +78,7 @@ 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() } +@Deprecated( + message = "Use wireViewModelScoped to avoid exposing the DI implementation in local APIs.", + replaceWith = ReplaceWith("wireViewModelScoped(arguments, clearDelay)") +) +@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") +@Composable +inline fun > + hiltViewModelScoped(arguments: R, clearDelay: Duration? = null): S where T : ViewModel, T : S = + wireViewModelScoped(arguments = arguments, clearDelay = clearDelay) + +@Deprecated( + message = "Use wireViewModelScoped to avoid exposing the DI implementation in local APIs.", + replaceWith = ReplaceWith("wireViewModelScoped(arguments, keyInScopeResolver, clearDelay)") +) +@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") +@Composable +inline fun > + hiltViewModelScoped( + arguments: R, + noinline keyInScopeResolver: KeyInScopeResolver, + clearDelay: Duration? = null, +): S where T : ViewModel, T : S = + wireViewModelScoped( + arguments = arguments, + keyInScopeResolver = keyInScopeResolver, + clearDelay = clearDelay + ) + +@Deprecated( + message = "Use wireViewModelScoped to avoid exposing the DI implementation in local APIs.", + replaceWith = ReplaceWith("wireViewModelScoped()") +) +@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") +@Composable +inline fun hiltViewModelScoped(): S where T : ViewModel, T : S = + wireViewModelScoped() + val espresso get() = try { Class.forName("androidx.test.espresso.Espresso") 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 bc4d2cc6512..eb6952e0759 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,7 +41,7 @@ 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.di.wireViewModel import com.wire.android.R import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -77,7 +77,7 @@ fun CreateAccountCodeScreen( navigator: Navigator, args: CreateAccountNavArgs, createAccountCodeViewModel: CreateAccountCodeViewModel = - hiltViewModel( + wireViewModel( creationCallback = { factory -> factory.create(args) } ) ) { 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 0b27e973b4a..a228b2b5dc0 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,7 +44,7 @@ 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.di.wireViewModel import com.wire.android.R import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -76,7 +76,7 @@ fun CreateAccountDetailsScreen( navigator: Navigator, args: CreateAccountNavArgs, createAccountDetailsViewModel: CreateAccountDetailsViewModel = - hiltViewModel( + wireViewModel( creationCallback = { factory -> factory.create(args) } ) ) { 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 27369f2079d..1a48d0ab390 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,7 +50,7 @@ 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.di.wireViewModel import com.wire.android.R import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -87,7 +87,7 @@ fun CreateAccountEmailScreen( navigator: Navigator, args: CreateAccountNavArgs, createAccountEmailViewModel: CreateAccountEmailViewModel = - hiltViewModel( + wireViewModel( creationCallback = { factory -> factory.create(args) } ) ) { 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 97ddf3d8c63..bf28883738b 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,7 +41,7 @@ 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.di.wireViewModel import com.wire.android.R import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -66,7 +66,7 @@ fun CreatePersonalAccountOverviewScreen( navigator: Navigator, args: CreateAccountOverviewNavArgs, viewModel: CreateAccountOverviewViewModel = - hiltViewModel( + wireViewModel( creationCallback = { factory -> factory.create(args) } ) ) { @@ -79,7 +79,7 @@ fun CreateTeamAccountOverviewScreen( navigator: Navigator, args: CreateAccountOverviewNavArgs, viewModel: CreateAccountOverviewViewModel = - hiltViewModel( + wireViewModel( creationCallback = { factory -> factory.create(args) } ) ) { 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 8262758c679..de1474c3555 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.wireViewModel import com.wire.android.R import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -54,7 +54,7 @@ fun CreateAccountSummaryScreen( navigator: Navigator, args: CreateAccountSummaryNavArgs, viewModel: CreateAccountSummaryViewModel = - hiltViewModel( + wireViewModel( creationCallback = { factory -> factory.create(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..bcaeb71715e 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,7 +32,7 @@ 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.di.wireViewModel import com.wire.android.R import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -58,7 +58,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun CreateAccountUsernameScreen( navigator: Navigator, - viewModel: CreateAccountUsernameViewModel = hiltViewModel() + viewModel: CreateAccountUsernameViewModel = wireViewModel() ) { UsernameContent( textState = viewModel.textState, 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..1a5e69895b0 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,7 +39,7 @@ 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.di.wireViewModel import com.wire.android.R import com.wire.android.feature.NavigationSwitchAccountActions import com.wire.android.navigation.BackStackMode @@ -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 = wireViewModel(), + clearSessionViewModel: ClearSessionViewModel = wireViewModel(), ) { 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..dbb18412692 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.wireViewModel 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 = wireViewModel() ) = VerificationCodeScreenContent( viewModel.secondFactorVerificationCodeTextState, viewModel.secondFactorVerificationCodeState, 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..44c3b3e6f1f 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.wireViewModel 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 = wireViewModel(), + clearSessionViewModel: ClearSessionViewModel = wireViewModel(), ) { 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..d11fa642ed6 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.wireViewModel import com.wire.android.ui.authentication.verificationcode.VerificationCodeScreenContent @Composable fun RemoveDeviceVerificationCodeScreen( - viewModel: RemoveDeviceViewModel = hiltViewModel() + viewModel: RemoveDeviceViewModel = wireViewModel() ) = VerificationCodeScreenContent( viewModel.secondFactorVerificationCodeTextState, viewModel.secondFactorVerificationCodeState, 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/LoginScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt index 08f35a9b830..f1d784a9954 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,7 +44,7 @@ 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.wire.android.di.wireViewModel import com.ramcosta.composedestinations.generated.app.destinations.E2EIEnrollmentScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination import com.ramcosta.composedestinations.generated.app.destinations.InitialSyncScreenDestination @@ -86,7 +86,7 @@ import kotlinx.coroutines.launch fun LoginScreen( navigator: Navigator, loginNavArgs: LoginNavArgs, - loginEmailViewModel: LoginEmailViewModel = hiltViewModel( + loginEmailViewModel: LoginEmailViewModel = wireViewModel( creationCallback = { factory -> factory.create(loginNavArgs) } ) ) { @@ -272,7 +272,7 @@ private fun PreviewLoginScreen() = WireTheme { onSuccess = { _, _ -> }, onRemoveDeviceNeeded = {}, loginNavArgs = LoginNavArgs(), - loginEmailViewModel = hiltViewModel(), + loginEmailViewModel = wireViewModel(), ssoLoginResult = null, ssoCodeAutoLogin = null ) 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..8e2fa94a810 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/SavedStateLoginSavedInputStore.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.authentication.login + +import androidx.lifecycle.SavedStateHandle +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +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 + } +} + +@Module +@InstallIn(ViewModelComponent::class) +object LoginSavedInputStoreModule { + @Provides + fun provideLoginSavedInputStore(savedStateHandle: SavedStateHandle): LoginSavedInputStore = + SavedStateLoginSavedInputStore(savedStateHandle) +} 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 4e6f3c12bff..07defed5d7c 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,13 +25,13 @@ 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 @@ -78,7 +78,7 @@ class LoginEmailViewModel @AssistedInject constructor( @Assisted 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, @@ -118,13 +118,13 @@ class LoginEmailViewModel @AssistedInject 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(), @@ -412,7 +412,6 @@ class LoginEmailViewModel @AssistedInject 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/sso/LoginSSOScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt index 6efdf2012fd..97b7b7eb6c3 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 @@ -41,7 +41,7 @@ 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.di.wireViewModel import com.wire.android.R import com.wire.android.ui.authentication.login.LoginErrorDialog import com.wire.android.ui.authentication.login.LoginState @@ -66,7 +66,7 @@ fun LoginSSOScreen( loginNavArgs: com.wire.android.ui.authentication.login.LoginNavArgs, ssoLoginResult: DeepLinkResult.SSOLogin?, ssoCodeAutoLogin: com.wire.android.ui.authentication.login.SSOCodeAutoLogin?, - loginSSOViewModel: LoginSSOViewModel = hiltViewModel( + loginSSOViewModel: LoginSSOViewModel = wireViewModel( creationCallback = { factory -> factory.create(loginNavArgs) } ), scrollState: ScrollState = rememberScrollState() 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 660b3d137ee..8e063c60648 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,7 +24,6 @@ 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.appLogger import com.wire.android.config.DefaultServerConfig @@ -33,6 +32,7 @@ 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 @@ -71,7 +71,7 @@ import kotlinx.coroutines.withContext @Suppress("LongParameterList", "TooManyFunctions") @HiltViewModel(assistedFactory = LoginSSOViewModel.Factory::class) class LoginSSOViewModel : LoginViewModel { - private val savedStateHandle: SavedStateHandle + private val savedInputStore: LoginSavedInputStore val addAuthenticatedUser: AddAuthenticatedUserUseCase private val validateEmailUseCase: ValidateEmailUseCase private val ssoExtension: LoginSSOViewModelExtension @@ -83,7 +83,7 @@ class LoginSSOViewModel : LoginViewModel { constructor( loginNavArgs: LoginNavArgs, - savedStateHandle: SavedStateHandle, + savedInputStore: LoginSavedInputStore, addAuthenticatedUser: AddAuthenticatedUserUseCase, validateEmailUseCase: ValidateEmailUseCase, coreLogic: CoreLogic, @@ -95,7 +95,7 @@ class LoginSSOViewModel : LoginViewModel { dispatchers: DispatcherProvider, ) : this( loginNavArgs, - savedStateHandle, + savedInputStore, addAuthenticatedUser, validateEmailUseCase, coreLogic, @@ -110,7 +110,7 @@ class LoginSSOViewModel : LoginViewModel { @AssistedInject constructor( @Assisted loginNavArgs: LoginNavArgs, - savedStateHandle: SavedStateHandle, + savedInputStore: LoginSavedInputStore, addAuthenticatedUser: AddAuthenticatedUserUseCase, validateEmailUseCase: ValidateEmailUseCase, @KaliumCoreLogic coreLogic: CoreLogic, @@ -122,7 +122,7 @@ class LoginSSOViewModel : LoginViewModel { dispatchers: DispatcherProvider, ) : this( loginNavArgs, - savedStateHandle, + savedInputStore, addAuthenticatedUser, validateEmailUseCase, coreLogic, @@ -136,7 +136,7 @@ class LoginSSOViewModel : LoginViewModel { private constructor( loginNavArgs: LoginNavArgs, - savedStateHandle: SavedStateHandle, + savedInputStore: LoginSavedInputStore, addAuthenticatedUser: AddAuthenticatedUserUseCase, validateEmailUseCase: ValidateEmailUseCase, coreLogic: CoreLogic, @@ -154,7 +154,7 @@ class LoginSSOViewModel : LoginViewModel { LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), serverConfig ) { - this.savedStateHandle = savedStateHandle + this.savedInputStore = savedInputStore this.addAuthenticatedUser = addAuthenticatedUser this.validateEmailUseCase = validateEmailUseCase this.ssoExtension = ssoExtension @@ -174,13 +174,13 @@ class LoginSSOViewModel : LoginViewModel { } private fun observeSSOCodeInput() { - ssoTextState.setTextAndPlaceCursorAtEnd(savedStateHandle[SSO_CODE_SAVED_STATE_KEY] ?: String.EMPTY) + 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() } } } @@ -440,7 +440,6 @@ class LoginSSOViewModel : LoginViewModel { } 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/welcome/WelcomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt index a331e2f36d5..a6300ce4edc 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,7 +66,7 @@ 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.di.wireViewModel import com.wire.android.BuildConfig.ENABLE_NEW_REGISTRATION import com.wire.android.R import com.wire.android.config.LocalCustomUiConfigurationProvider @@ -114,7 +114,7 @@ import kotlinx.coroutines.flow.scan fun WelcomeScreen( navigator: Navigator, args: WelcomeNavArgs, - viewModel: WelcomeViewModel = hiltViewModel( + viewModel: WelcomeViewModel = wireViewModel( creationCallback = { factory -> factory.create(args) } ) ) { 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..7f0c69c344c 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,7 +22,7 @@ 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 @@ -42,7 +42,7 @@ fun AddContactButton( userName: String, modifier: Modifier = Modifier, viewModel: ConnectionActionButtonViewModel = - hiltViewModelScoped< + wireViewModelScoped< ConnectionActionButtonViewModelImpl, ConnectionActionButtonViewModel, ConnectionActionButtonArgs, 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..3a42b978ddb 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,7 +56,7 @@ fun SecurityClassificationBannerForConversation( conversationId: ConversationId, modifier: Modifier = Modifier, viewModel: SecurityClassificationViewModel = - hiltViewModelScoped< + wireViewModelScoped< SecurityClassificationViewModelImpl, SecurityClassificationViewModel, SecurityClassificationArgs, @@ -74,7 +74,7 @@ fun SecurityClassificationBannerForUser( userId: UserId, modifier: Modifier = Modifier, viewModel: SecurityClassificationViewModel = - hiltViewModelScoped< + wireViewModelScoped< SecurityClassificationViewModelImpl, SecurityClassificationViewModel, SecurityClassificationArgs, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt index 8435f34b163..2f19f44fba1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt @@ -24,7 +24,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.HandleActions import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout import com.wire.android.ui.common.bottomsheet.WireModalSheetState @@ -58,7 +58,7 @@ fun ConversationOptionsModalSheetLayout( onPromoteAdmin: (ConversationId) -> Unit = {}, openConversationDebugMenu: (ConversationId) -> Unit = {}, viewModel: ConversationOptionsMenuViewModel = - hiltViewModelScoped() + wireViewModelScoped() ) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current 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..ec4262f7d6d 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 @@ -73,7 +73,7 @@ fun ConnectionActionButton( onConnectionRequestIgnored: (String) -> Unit = {}, onOpenConversation: (ConversationId) -> Unit = {}, viewModel: ConnectionActionButtonViewModel = - hiltViewModelScoped< + wireViewModelScoped< ConnectionActionButtonViewModelImpl, ConnectionActionButtonViewModel, ConnectionActionButtonArgs, 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..1f8edbc0e5e 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/DebugScreen.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt index 42ff2992445..ad082ca03af 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 @@ -44,7 +44,7 @@ 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.wireViewModelScoped import com.wire.android.model.Clickable import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -149,7 +149,7 @@ internal fun UserDebugContent( fun DangerOptions( modifier: Modifier = Modifier, exportObfuscatedCopyViewModel: ExportObfuscatedCopyViewModel = - hiltViewModelScoped() + wireViewModelScoped() ) { Column(modifier = modifier) { 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..ca61d3c939c 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 @@ -74,14 +74,14 @@ private const val ANIMATION_SPEED_MILLIS = 1_500 fun UsersTypingIndicatorForConversation( conversationId: ConversationId, viewModel: TypingIndicatorViewModel = - hiltViewModelScoped< - TypingIndicatorViewModelImpl, - TypingIndicatorViewModel, - TypingIndicatorArgs, - TypingIndicatorViewModelImpl.Factory - >( - TypingIndicatorArgs(conversationId) - ) + wireViewModelScoped< + TypingIndicatorViewModelImpl, + TypingIndicatorViewModel, + TypingIndicatorArgs, + TypingIndicatorViewModelImpl.Factory + >( + TypingIndicatorArgs(conversationId) + ) ) { UsersTypingIndicator(usersTyping = viewModel.state().usersTyping) } 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..ba985d6a406 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,7 +62,7 @@ fun MessageOptionsModalSheetLayout( onDownloadAssetClick: (messageId: String) -> Unit, onOpenAssetClick: (messageId: String) -> Unit, viewModel: MessageOptionsMenuViewModel = - hiltViewModelScoped< + wireViewModelScoped< MessageOptionsMenuViewModelImpl, MessageOptionsMenuViewModel, MessageOptionsMenuArgs, 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..d1046a77d28 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 @@ -622,7 +622,7 @@ private fun QuotedImageThumbnail( val keyInScopeResolver = LocalAssetLocalPathKeyInScopeResolver.current val viewModel: AssetLocalPathViewModel = if (keyInScopeResolver != null && keyInScopeResolver(args.key)) { - hiltViewModelScoped< + wireViewModelScoped< AssetLocalPathViewModelImpl, AssetLocalPathViewModel, AssetLocalPathArgs, @@ -632,7 +632,7 @@ private fun QuotedImageThumbnail( keyInScopeResolver = keyInScopeResolver, ) } else { - hiltViewModelScoped< + wireViewModelScoped< AssetLocalPathViewModelImpl, AssetLocalPathViewModel, AssetLocalPathArgs, 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 18c8ef2523b..479280923d4 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 @@ -172,7 +172,7 @@ fun MessageButtonsContent( messageStyle: MessageStyle, modifier: Modifier = Modifier, viewModel: CompositeMessageViewModel = - hiltViewModelScoped< + wireViewModelScoped< CompositeMessageViewModelImpl, CompositeMessageViewModel, CompositeMessageArgs, 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..dcc7302e38f 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,7 +65,7 @@ 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 @@ -182,14 +182,14 @@ private fun UploadedAudioMessage( ) { val keyInScopeResolver = LocalAudioMessageKeyInScopeResolver.current val viewModel: AudioMessageViewModel = if (keyInScopeResolver != null) { - hiltViewModelScoped< + wireViewModelScoped< AudioMessageViewModelImpl, AudioMessageViewModel, AudioMessageArgs, AudioMessageViewModelImpl.Factory >(audioMessageArgs, keyInScopeResolver) } else { - hiltViewModelScoped< + wireViewModelScoped< AudioMessageViewModelImpl, AudioMessageViewModel, AudioMessageArgs, 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..1eeed417d3e 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,7 +31,7 @@ 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 @@ -245,7 +245,7 @@ fun SelfDeletingMessageAction( conversationId: ConversationId, onButtonClicked: (SelfDeletionTimer) -> Unit, viewModel: SelfDeletingMessageActionViewModel = - hiltViewModelScoped< + wireViewModelScoped< SelfDeletingMessageActionViewModelImpl, SelfDeletingMessageActionViewModel, SelfDeletingMessageActionArgs, 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..f762ccab550 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 @@ -184,7 +184,7 @@ private fun InputContent( onPlusClick: () -> Unit, modifier: Modifier = Modifier, viewModel: SelfDeletingMessageActionViewModel = - hiltViewModelScoped< + wireViewModelScoped< SelfDeletingMessageActionViewModelImpl, SelfDeletingMessageActionViewModel, SelfDeletingMessageActionArgs, 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..4fec3922c93 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 = { 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..f28440e3eca 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 @@ -52,7 +52,7 @@ fun RecordAudioComponent( modifier: Modifier = Modifier, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current ) { - val viewModel: RecordAudioViewModel = hiltViewModelScoped() + val viewModel: RecordAudioViewModel = wireViewModelScoped() val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current 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..a2521a52263 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,7 @@ fun JoinConversationViaDeepLinkDialog( onFlowCompleted: (conversationId: ConversationId?) -> Unit, modifier: Modifier = Modifier, ) { - val viewModel = hiltViewModelScoped() + val viewModel = wireViewModelScoped() val isLoading: Boolean by remember { derivedStateOf { viewModel.state is JoinViaDeepLinkDialogState.Loading } 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..d5345da35ab 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, don't use wireViewModel() ) { clearAutofillTree() LoginStateNavigationAndDialogs(loginEmailViewModel, navigator) 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 f2818fd8102..7780bfe3f07 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.wireViewModel 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,7 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun NewLoginScreen( navigator: Navigator, navArgs: LoginNavArgs, - viewModel: NewLoginViewModel = hiltViewModel( + viewModel: NewLoginViewModel = wireViewModel( creationCallback = { factory -> factory.create(navArgs) } ) ) { @@ -142,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 c9370a4bdb6..3e98ce1fa19 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 @@ -24,7 +24,6 @@ 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.appLogger import com.wire.android.datastore.UserDataStoreProvider @@ -34,9 +33,9 @@ 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 @@ -67,11 +66,9 @@ 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 javax.inject.Named @Suppress("LongParameterList", "TooManyFunctions") @@ -80,7 +77,7 @@ 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, @@ -96,7 +93,7 @@ class NewLoginViewModel( @Assisted loginNavArgs: LoginNavArgs, validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase, @KaliumCoreLogic coreLogic: CoreLogic, - savedStateHandle: SavedStateHandle, + savedInputStore: LoginSavedInputStore, addAuthenticatedUser: AddAuthenticatedUserUseCase, clientScopeProviderFactory: ClientScopeProvider.Factory, userDataStoreProvider: UserDataStoreProvider, @@ -109,7 +106,7 @@ class NewLoginViewModel( loginNavArgs, validateEmailOrSSOCode, coreLogic, - savedStateHandle, + savedInputStore, clientScopeProviderFactory, userDataStoreProvider, LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), @@ -143,12 +140,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 @@ -173,7 +170,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") @@ -317,18 +314,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) { 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 590ef4daf33..07a7fa1ff2c 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,7 +46,7 @@ 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.di.wireViewModel import com.wire.android.BuildConfig import com.wire.android.BuildConfig.ENABLE_NEW_REGISTRATION import com.wire.android.R @@ -105,7 +105,7 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun NewLoginPasswordScreen( navigator: Navigator, navArgs: LoginNavArgs, - loginEmailViewModel: LoginEmailViewModel = hiltViewModel( + loginEmailViewModel: LoginEmailViewModel = wireViewModel( creationCallback = { factory -> factory.create(navArgs) } ) ) { 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 61b313320ea..6d4c1e28b3d 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,7 +41,7 @@ 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.di.wireViewModel import com.wire.android.R import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -82,7 +82,7 @@ fun CreateAccountVerificationCodeScreen( navigator: Navigator, args: CreateAccountDataNavArgs, createAccountCodeVerificationViewModel: CreateAccountVerificationCodeViewModel = - hiltViewModel( + wireViewModel( creationCallback = { factory -> factory.create(args) } ) ) { 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 167aeb5eafa..6b1eaf7fd4c 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,7 +53,7 @@ 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.di.wireViewModel import com.wire.android.R import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -97,7 +97,7 @@ fun CreateAccountDataDetailScreen( navigator: Navigator, args: CreateAccountDataNavArgs, createAccountDataDetailViewModel: CreateAccountDataDetailViewModel = - hiltViewModel( + wireViewModel( creationCallback = { factory -> factory.create(args) } ) ) { 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 9bc10598e66..7b871c83bd4 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,7 +46,7 @@ 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.di.wireViewModel import com.wire.android.R import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -84,7 +84,7 @@ fun CreateAccountSelectorScreen( navigator: Navigator, args: CreateAccountSelectorNavArgs, viewModel: CreateAccountSelectorViewModel = - hiltViewModel( + wireViewModel( creationCallback = { factory -> factory.create(args) } ) ) { 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 3b6f07aa957..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,6 +36,7 @@ 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.wire.android.util.EMPTY import com.wire.android.util.newServerConfig @@ -814,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 @@ -852,9 +852,9 @@ 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 @@ -876,7 +876,7 @@ class LoginEmailViewModelTest { 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 bb0f703d44a..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,7 +19,6 @@ 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.wire.android.assertions.shouldBeEqualTo import com.wire.android.assertions.shouldBeInstanceOf @@ -34,6 +33,7 @@ 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 @@ -1059,7 +1059,7 @@ class LoginSSOViewModelTest { private class Arrangement { @MockK - lateinit var savedStateHandle: SavedStateHandle + lateinit var savedInputStore: LoginSavedInputStore @MockK lateinit var ssoInitiateLoginUseCase: SSOInitiateLoginUseCase @@ -1117,8 +1117,8 @@ class LoginSSOViewModelTest { init { MockKAnnotations.init(this) - 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 coEvery { @@ -1229,7 +1229,7 @@ class LoginSSOViewModelTest { fun arrange(): Pair { val viewModel = LoginSSOViewModel( loginNavArgs = LoginNavArgs(loginPasswordPath = LoginPasswordPath(SERVER_CONFIG.links)), - savedStateHandle = savedStateHandle, + savedInputStore = savedInputStore, addAuthenticatedUser = addAuthenticatedUserUseCase, validateEmailUseCase = validateEmailUseCase, coreLogic = coreLogic, 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 2963330f370..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 @@ -2,7 +2,6 @@ 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.wire.android.config.CoroutineTestExtension import com.wire.android.config.SnapshotExtension @@ -13,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 @@ -655,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 @@ -685,12 +685,8 @@ class NewLoginViewModelTest { init { MockKAnnotations.init(this, relaxUnitFun = true) - every { - savedStateHandle.get(any()) - } returns null - every { - savedStateHandle[any()] = any() - } returns Unit + every { savedInputStore.userIdentifier } returns null + every { savedInputStore.userIdentifier = any() } returns Unit every { coreLogic.getGlobalScope().deleteSession } returns deleteSessionUseCase every { coreLogic.getSessionScope(any()).logout } returns logoutUseCase } @@ -830,16 +826,12 @@ class NewLoginViewModelTest { } fun withEmptyUserIdentifierAndNoPreFilledIdentifier() = apply { - every { - savedStateHandle.get(any()) - } returns null + 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 { @@ -900,7 +892,7 @@ class NewLoginViewModelTest { loginNavArgs, validateEmailOrSSOCodeUseCase, coreLogic, - savedStateHandle, + savedInputStore, clientScopeProviderFactory, userDataStoreProvider, loginViewModelExtension, 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 d611c54bd60..75b63caff5e 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,7 @@ import com.wire.android.ui.common.topappbar.search.SearchTopBar fun AllFilesScreen( navigator: WireNavigator, modifier: Modifier = Modifier, - viewModel: CellViewModel = hiltViewModel( + viewModel: CellViewModel = wireCellsViewModel( creationCallback = { factory -> factory.create(CellFilesNavArgs(), null) } ), ) { 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..cde06706769 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,6 +40,8 @@ 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.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.ViewModel import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.itemContentType @@ -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 wireCellsViewModel( + key: String? = null, + noinline creationCallback: (VMF) -> VM, +): VM = hiltViewModel( + key = key, + creationCallback = creationCallback +) + @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun CellFilesScreen( 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 1392295ddf6..d4d2a5d0c22 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 @@ -102,7 +101,7 @@ fun ConversationFilesScreen( navigator: WireNavigator, animatedVisibilityScope: AnimatedVisibilityScope, args: CellFilesNavArgs, - viewModel: CellViewModel = hiltViewModel( + viewModel: CellViewModel = wireCellsViewModel( creationCallback = { factory -> factory.create(args, null) } ), ) { 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 771a84a7fa3..817b0dde79d 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,7 @@ fun ConversationFilesWithSlideInTransitionScreen( navigator: WireNavigator, cellFilesNavArgs: CellFilesNavArgs, animatedVisibilityScope: AnimatedVisibilityScope, - viewModel: CellViewModel = hiltViewModel( + viewModel: CellViewModel = wireCellsViewModel( creationCallback = { factory -> factory.create(cellFilesNavArgs, null) } ), ) { 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 cf16a5e0cfa..6fc786a101f 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.wireCellsViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.common.FileNameError @@ -65,7 +65,7 @@ fun CreateFileScreen( resultNavigator: ResultBackNavigator, args: CreateFileScreenNavArgs, modifier: Modifier = Modifier, - createFileViewModel: CreateFileViewModel = hiltViewModel( + createFileViewModel: CreateFileViewModel = wireCellsViewModel( creationCallback = { factory -> factory.create(args) } ) ) { 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 1d9e72ca172..c8dbebc90dc 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.wireCellsViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.common.FileNameError @@ -70,9 +70,10 @@ fun CreateFolderScreen( resultNavigator: ResultBackNavigator, args: CreateFolderScreenNavArgs, modifier: Modifier = Modifier, - createFolderViewModel: CreateFolderViewModel = hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + createFolderViewModel: CreateFolderViewModel = + wireCellsViewModel( + creationCallback = { factory -> factory.create(args) } + ) ) { val showErrorDialog = remember { mutableStateOf(false) } 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 e099edaf9be..10dca795c55 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.wireCellsViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import com.wire.android.feature.cells.R @@ -80,9 +80,10 @@ fun MoveToFolderScreen( createFolderResultRecipient: ResultRecipient, args: MoveToFolderNavArgs, modifier: Modifier = Modifier, - moveToFolderViewModel: MoveToFolderViewModel = hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + moveToFolderViewModel: MoveToFolderViewModel = + wireCellsViewModel( + creationCallback = { factory -> factory.create(args) } + ) ) { val context = LocalContext.current val viewState by moveToFolderViewModel.state.collectAsState() 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 6cfea64bc84..891fc86c765 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.wireCellsViewModel import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient import com.ramcosta.composedestinations.spec.TypedDestinationSpec @@ -83,7 +83,7 @@ fun PublicLinkScreen( onExpirationChange: ResultRecipient, args: PublicLinkNavArgs, modifier: Modifier = Modifier, - viewModel: PublicLinkViewModel = hiltViewModel( + viewModel: PublicLinkViewModel = wireCellsViewModel( creationCallback = { factory -> factory.create(args) } ), ) { 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 d819e8cb089..3266ac2d814 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.wireCellsViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.common.WireCellErrorDialog @@ -74,7 +74,7 @@ internal fun PublicLinkExpirationScreen( args: PublicLinkExpirationScreenNavArgs, modifier: Modifier = Modifier, viewModel: PublicLinkExpirationScreenViewModel = - hiltViewModel( + wireCellsViewModel( creationCallback = { factory -> factory.create(args) } ), ) { 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 8ff93747c74..4fbfdbc6cd2 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.wireCellsViewModel import com.ramcosta.composedestinations.result.ResultBackNavigator import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.common.WireCellErrorDialog @@ -71,7 +71,7 @@ internal fun PublicLinkPasswordScreen( args: PublicLinkPasswordNavArgs, modifier: Modifier = Modifier, viewModel: PublicLinkPasswordScreenViewModel = - hiltViewModel( + wireCellsViewModel( creationCallback = { factory -> factory.create(args) } ), ) { 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 1d5752bd78c..a0e78d45d3e 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.wireCellsViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.CellFilesNavArgs @@ -56,7 +56,7 @@ fun RecycleBinScreen( navigator: WireNavigator, args: CellFilesNavArgs, modifier: Modifier = Modifier, - cellViewModel: CellViewModel = hiltViewModel( + cellViewModel: CellViewModel = wireCellsViewModel( creationCallback = { factory -> factory.create(args, null) } ) ) { 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 e7137fe0374..72c24d3c18e 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.wireCellsViewModel 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 @@ -66,7 +66,7 @@ fun RenameNodeScreen( navigator: WireNavigator, args: RenameNodeNavArgs, modifier: Modifier = Modifier, - renameNodeViewModel: RenameNodeViewModel = hiltViewModel( + renameNodeViewModel: RenameNodeViewModel = wireCellsViewModel( creationCallback = { factory -> factory.create(args) } ) ) { 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 4f92ccb0b98..463079c5910 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.wireCellsViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.collectAsLazyPagingItems import com.ramcosta.composedestinations.generated.cells.destinations.AddRemoveTagsScreenDestination @@ -77,9 +77,10 @@ fun SearchScreen( cellViewModel: CellViewModel, args: SearchNavArgs, modifier: Modifier = Modifier, - searchScreenViewModel: SearchScreenViewModel = hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + searchScreenViewModel: SearchScreenViewModel = + wireCellsViewModel( + creationCallback = { factory -> factory.create(args) } + ), ) { val uiState by searchScreenViewModel.uiState.collectAsStateWithLifecycle() 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 327fb15ba5b..4fc32b44d89 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.wireCellsViewModel import com.wire.android.feature.cells.R import com.wire.android.model.ClickBlockParams import com.wire.android.navigation.WireNavigator @@ -80,7 +80,7 @@ fun AddRemoveTagsScreen( args: AddRemoveTagsNavArgs, modifier: Modifier = Modifier, addRemoveTagsViewModel: AddRemoveTagsViewModel = - hiltViewModel( + wireCellsViewModel( creationCallback = { factory -> factory.create(args) } ), ) { 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 e3370bc2fab..9b8ea4d6e93 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.wireCellsViewModel 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 @@ -74,7 +74,7 @@ fun VersionHistoryScreen( args: VersionHistoryNavArgs, modifier: Modifier = Modifier, versionHistoryViewModel: VersionHistoryViewModel = - hiltViewModel( + wireCellsViewModel( creationCallback = { factory -> factory.create(args) } ) ) { From 2d0e32b8689b6a68528d18a6cc52fa72c7434a14 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 15:18:12 +0200 Subject: [PATCH 04/46] chore: use JDK 21 for project builds --- .java-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.java-version b/.java-version index 03b6389f32a..5f39e914469 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -17.0 +21.0 From 2d26d07cdda56983720759c372a78a2f77fb6b14 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 15:18:48 +0200 Subject: [PATCH 05/46] feat: add Metro ViewModel pilot setup --- app/build.gradle.kts | 1 + .../metro/MetroViewModelMigrationTemplate.kt | 87 +++++++++++++++++++ .../wire/android/di/metro/WireMetroGraph.kt | 28 ++++++ .../CheckAssetRestrictionsViewModelFactory.kt | 25 ++++++ .../main/kotlin/KmpLibraryConventionPlugin.kt | 1 - build.gradle.kts | 1 + gradle/libs.versions.toml | 2 + wireone-kmp/build.gradle.kts | 14 +-- 8 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelMigrationTemplate.kt create mode 100644 app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/CheckAssetRestrictionsViewModelFactory.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3fe03d4ad2c..fe22271526f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,6 +29,7 @@ plugins { id(libs.plugins.wire.hilt.get().pluginId) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) + alias(libs.plugins.metro) alias(libs.plugins.compose.compiler) id(libs.plugins.aboutLibraries.get().pluginId) 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..2eeb0506456 --- /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 Hilt 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 Hilt-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 Hilt and Metro 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 Hilt annotations stay until the runtime 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..9e667665bcd --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.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.di.metro + +import com.wire.android.ui.home.conversations.media.CheckAssetRestrictionsViewModelFactory +import dev.zacsweers.metro.DependencyGraph + +abstract class WireMetroScope private constructor() + +@DependencyGraph(WireMetroScope::class) +interface WireMetroGraph { + val checkAssetRestrictionsViewModelFactory: CheckAssetRestrictionsViewModelFactory +} 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/build-logic/plugins/src/main/kotlin/KmpLibraryConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/KmpLibraryConventionPlugin.kt index 10fbc68841a..8bf517c0d17 100644 --- a/build-logic/plugins/src/main/kotlin/KmpLibraryConventionPlugin.kt +++ b/build-logic/plugins/src/main/kotlin/KmpLibraryConventionPlugin.kt @@ -44,7 +44,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/gradle/libs.versions.toml b/gradle/libs.versions.toml index d93d255193b..93270e0abf6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ desugaring = "2.1.5" firebaseBOM = "34.7.0" fragment = "1.5.6" resaca = "5.0.2" +metro = "0.12.1" bundlizer = "0.8.0" squareup-javapoet = "1.13.0" visibilityModifiers = "1.1.0" @@ -133,6 +134,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" } diff --git a/wireone-kmp/build.gradle.kts b/wireone-kmp/build.gradle.kts index 48fd51ba50b..860c1e8db9b 100644 --- a/wireone-kmp/build.gradle.kts +++ b/wireone-kmp/build.gradle.kts @@ -25,21 +25,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") + } } } From 2e15e04c3c0f4a6aae4037589af0afefc598ef6a Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 16:29:35 +0200 Subject: [PATCH 06/46] feat: add Metro factories for ViewModels --- app/build.gradle.kts | 1 - .../AudioMessageViewModelFactory.kt | 33 ++++++ .../ui/CallFeedbackViewModelFactory.kt | 41 +++++++ .../ui/WireActivityViewModelFactory.kt | 109 ++++++++++++++++++ .../AnalyticsUsageViewModelFactory.kt | 36 ++++++ .../code/CreateAccountCodeViewModelFactory.kt | 45 ++++++++ .../CreateAccountDetailsViewModelFactory.kt | 35 ++++++ .../CreateAccountEmailViewModelFactory.kt | 39 +++++++ .../CreateAccountOverviewViewModelFactory.kt | 31 +++++ .../CreateAccountSummaryViewModelFactory.kt | 27 +++++ .../CreateAccountUsernameViewModelFactory.kt | 39 +++++++ .../common/ClearSessionViewModelFactory.kt | 39 +++++++ .../RegisterDeviceViewModelFactory.kt | 44 +++++++ .../remove/RemoveDeviceViewModelFactory.kt | 48 ++++++++ .../login/LoginSavedInputStoreModule.kt | 33 ++++++ .../login/SavedStateLoginSavedInputStore.kt | 12 -- .../login/email/LoginEmailViewModelFactory.kt | 58 ++++++++++ .../login/sso/LoginSSOViewModelFactory.kt | 60 ++++++++++ .../welcome/WelcomeViewModelFactory.kt | 37 ++++++ .../calling/CallActivityViewModelFactory.kt | 40 +++++++ .../common/SharedCallingViewModelFactory.kt | 75 ++++++++++++ .../incoming/IncomingCallViewModelFactory.kt | 58 ++++++++++ .../ongoing/OngoingCallViewModelFactory.kt | 73 ++++++++++++ .../outgoing/OutgoingCallViewModelFactory.kt | 47 ++++++++ .../SecurityClassificationViewModelFactory.kt | 32 +++++ ...ConversationOptionsMenuViewModelFactory.kt | 80 +++++++++++++ .../CommonTopAppBarViewModelFactory.kt | 36 ++++++ .../ConnectionActionButtonViewModelFactory.kt | 50 ++++++++ .../debug/DebugDataOptionsViewModelFactory.kt | 76 ++++++++++++ .../ExportObfuscatedCopyViewModelFactory.kt | 37 ++++++ .../ui/debug/LogManagementViewModelFactory.kt | 33 ++++++ .../ui/debug/UserDebugViewModelFactory.kt | 46 ++++++++ .../DebugConversationViewModelFactory.kt | 43 +++++++ ...ConversationCryptoStatsViewModelFactory.kt | 30 +++++ .../DebugFeatureFlagsViewModelFactory.kt | 30 +++++ .../E2EIEnrollmentViewModelFactory.kt | 30 +++++ .../GetE2EICertificateViewModelFactory.kt | 37 ++++++ .../ui/home/AppSyncViewModelFactory.kt | 33 ++++++ .../android/ui/home/HomeViewModelFactory.kt | 46 ++++++++ .../ForgotLockScreenViewModelFactory.kt | 53 +++++++++ .../set/SetLockScreenViewModelFactory.kt | 45 ++++++++ ...AppUnlockWithBiometricsViewModelFactory.kt | 30 +++++ .../unlock/EnterLockScreenViewModelFactory.kt | 39 +++++++ .../CompositeMessageViewModelFactory.kt | 32 +++++ .../MessageAttachmentsViewModelFactory.kt | 55 +++++++++ .../ConversationBannerViewModelFactory.kt | 38 ++++++ .../call/ConversationCallViewModelFactory.kt | 70 +++++++++++ .../MessageComposerViewModelFactory.kt | 71 ++++++++++++ ...roupConversationDetailsViewModelFactory.kt | 59 ++++++++++ .../EditGuestAccessViewModelFactory.kt | 65 +++++++++++ ...CreatePasswordGuestLinkViewModelFactory.kt | 37 ++++++ ...ditSelfDeletingMessagesViewModelFactory.kt | 46 ++++++++ ...ditConversationMetadataViewModelFactory.kt | 37 ++++++ ...onversationParticipantsViewModelFactory.kt | 35 ++++++ .../UpdateAppsAccessViewModelFactory.kt | 46 ++++++++ .../UpdateChannelAccessViewModelFactory.kt | 34 ++++++ .../MessageOptionsMenuViewModelFactory.kt | 31 +++++ .../ConversationFoldersViewModelFactory.kt | 31 +++++ ...oveConversationToFolderViewModelFactory.kt | 34 ++++++ .../folder/NewFolderViewModelFactory.kt | 33 ++++++ .../info/ConversationInfoViewModelFactory.kt | 45 ++++++++ ...nversationAssetMessagesViewModelFactory.kt | 37 ++++++ .../preview/ImagesPreviewViewModelFactory.kt | 30 +++++ .../MessageDetailsViewModelFactory.kt | 34 ++++++ .../ConversationMessagesViewModelFactory.kt | 80 +++++++++++++ .../QuotedMultipartMessageViewModelFactory.kt | 30 +++++ .../draft/MessageDraftViewModelFactory.kt | 38 ++++++ .../item/AssetLocalPathViewModelFactory.kt | 34 ++++++ .../ConversationAssetPathsViewModelFactory.kt | 33 ++++++ .../ConversationMigrationViewModelFactory.kt | 32 +++++ .../MultipartAttachmentsViewModelFactory.kt | 51 ++++++++ .../search/SearchUserViewModelFactory.kt | 46 ++++++++ ...ddMembersToConversationViewModelFactory.kt | 35 ++++++ .../search/apps/SearchAppsViewModelFactory.kt | 50 ++++++++ ...rchConversationMessagesViewModelFactory.kt | 35 ++++++ .../SendMessageViewModelFactory.kt | 96 +++++++++++++++ .../typing/TypingIndicatorViewModelFactory.kt | 31 +++++ .../ConversationListCallViewModelFactory.kt | 40 +++++++ .../ConversationListViewModelFactory.kt | 69 +++++++++++ .../home/drawer/HomeDrawerViewModelFactory.kt | 40 +++++++ .../gallery/MediaGalleryViewModelFactory.kt | 49 ++++++++ ...lfDeletingMessageActionViewModelFactory.kt | 34 ++++++ .../IsFileSharingEnabledViewModelFactory.kt | 30 +++++ .../LocationPickerViewModelFactory.kt | 29 +++++ .../RecordAudioViewModelFactory.kt | 59 ++++++++++ .../NewConversationViewModelFactory.kt | 49 ++++++++ .../home/settings/SettingsViewModelFactory.kt | 39 +++++++ .../DependenciesViewModelFactory.kt | 29 +++++ .../licenses/LicensesViewModelFactory.kt | 29 +++++ .../account/MyAccountViewModelFactory.kt | 52 +++++++++ .../color/ChangeUserColorViewModelFactory.kt | 33 ++++++ .../DeleteAccountViewModelFactory.kt | 30 +++++ .../ChangeDisplayNameViewModelFactory.kt | 33 ++++++ .../ChangeEmailViewModelFactory.kt | 33 ++++++ .../VerifyEmailViewModelFactory.kt | 31 +++++ .../handle/ChangeHandleViewModelFactory.kt | 36 ++++++ .../CustomizationViewModelFactory.kt | 30 +++++ .../NetworkSettingsViewModelFactory.kt | 41 +++++++ .../BackupAndRestoreViewModelFactory.kt | 56 +++++++++ .../PrivacySettingsViewModelFactory.kt | 58 ++++++++++ ...FeatureFlagNotificationViewModelFactory.kt | 41 +++++++ .../home/whatsnew/WhatsNewViewModelFactory.kt | 29 +++++ .../InitialSyncViewModelFactory.kt | 43 +++++++ ...JoinConversationViaCodeViewModelFactory.kt | 30 +++++ .../LegalHoldDeactivatedViewModelFactory.kt | 32 +++++ .../LegalHoldRequestedViewModelFactory.kt | 35 ++++++ .../login/LoginFlowStateHolder.kt | 84 ++++++++++++++ .../newauthentication/login/LoginNavigator.kt | 51 ++++++++ .../login/NewLoginViewModelFactory.kt | 66 +++++++++++ ...AccountVerificationCodeViewModelFactory.kt | 48 ++++++++ ...CreateAccountDataDetailViewModelFactory.kt | 48 ++++++++ .../CreateAccountSelectorViewModelFactory.kt | 34 ++++++ .../about/AboutThisAppViewModelFactory.kt | 29 +++++ .../devices/DeviceDetailsViewModelFactory.kt | 60 ++++++++++ .../devices/SelfDevicesViewModelFactory.kt | 46 ++++++++ .../E2eiCertificateDetailsViewModelFactory.kt | 32 +++++ ...mportMediaAuthenticatedViewModelFactory.kt | 44 +++++++ .../AvatarPickerViewModelFactory.kt | 44 +++++++ .../OtherUserProfileScreenViewModelFactory.kt | 65 +++++++++++ .../qr/SelfQRCodeViewModelFactory.kt | 40 +++++++ .../self/SelfUserProfileViewModelFactory.kt | 95 +++++++++++++++ .../service/ServiceDetailsViewModelFactory.kt | 67 +++++++++++ .../TeamMigrationViewModelFactory.kt | 42 +++++++ .../SavedStateLoginSavedInputStoreTest.kt | 46 ++++++++ .../login/LoginFlowStateHolderTest.kt | 89 ++++++++++++++ .../login/LoginNavigatorTest.kt | 87 ++++++++++++++ build-logic/plugins/build.gradle.kts | 1 + .../AndroidApplicationConventionPlugin.kt | 1 + .../kotlin/AndroidLibraryConventionPlugin.kt | 1 + .../main/kotlin/KmpLibraryConventionPlugin.kt | 1 - core/runtime-kmp/build.gradle.kts | 23 ++++ .../android/runtime/viewmodel/IosViewModel.kt | 32 +++++ .../runtime/viewmodel/IosViewModelTest.kt | 60 ++++++++++ core/ui-common-kmp/build.gradle.kts | 1 + .../model/ImageAssetViewModelFactory.kt | 29 +++++ .../feature/cells/ui/CellViewModelFactory.kt | 64 ++++++++++ .../create/file/CreateFileViewModelFactory.kt | 38 ++++++ .../folder/CreateFolderViewModelFactory.kt | 32 +++++ .../MoveToFolderViewModelFactory.kt | 35 ++++++ .../publiclink/PublicLinkViewModelFactory.kt | 41 +++++++ ...licLinkExpirationScreenViewModelFactory.kt | 32 +++++ ...ublicLinkPasswordScreenViewModelFactory.kt | 41 +++++++ .../ui/rename/RenameNodeViewModelFactory.kt | 32 +++++ .../ui/search/SearchScreenViewModelFactory.kt | 44 +++++++ .../ui/tags/AddRemoveTagsViewModelFactory.kt | 38 ++++++ .../VersionHistoryViewModelFactory.kt | 53 +++++++++ .../ui/list/MeetingListViewModelFactory.kt | 32 +++++ .../MeetingOptionsMenuViewModelFactory.kt | 25 ++++ gradle/libs.versions.toml | 2 + wireone-kmp/build.gradle.kts | 1 + 150 files changed, 6258 insertions(+), 14 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/media/audiomessage/AudioMessageViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/analytics/AnalyticsUsageViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/devices/common/ClearSessionViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginSavedInputStoreModule.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/common/SharedCallingViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/LogManagementViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/cryptostats/ConversationCryptoStatsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/debug/featureflags/DebugFeatureFlagsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/GetE2EICertificateViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/HomeViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockScreenViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/appLock/set/SetLockScreenViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/AppUnlockWithBiometricsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockScreenViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/banner/ConversationBannerViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationCallViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/composer/MessageComposerViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/EditGuestAccessViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editguestaccess/createPasswordProtectedGuestLink/CreatePasswordGuestLinkViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/editselfdeletingmessages/EditSelfDeletingMessagesViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/metadata/EditConversationMetadataViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/participants/GroupConversationParticipantsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updateappsaccess/UpdateAppsAccessViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/details/updatechannelaccess/UpdateChannelAccessViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsMenuViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/ConversationFoldersViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/MoveConversationToFolderViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/folder/NewFolderViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/ConversationAssetMessagesViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/QuotedMultipartMessageViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/draft/MessageDraftViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/AssetLocalPathViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/ConversationAssetPathsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/migration/ConversationMigrationViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/search/adddembertoconversation/AddMembersToConversationViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/sendmessage/SendMessageViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/typing/TypingIndicatorViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListCallViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/drawer/HomeDrawerViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/actions/SelfDeletingMessageActionViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/IsFileSharingEnabledViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/location/LocationPickerViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/SettingsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/about/licenses/LicensesViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/account/MyAccountViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/account/color/ChangeUserColorViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/account/deleteAccount/DeleteAccountViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/account/displayname/ChangeDisplayNameViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/updateEmail/ChangeEmailViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/account/handle/ChangeHandleViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/appearance/CustomizationViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/settings/privacy/PrivacySettingsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/initialsync/InitialSyncViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaCodeViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/deactivated/LegalHoldDeactivatedViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/legalhold/dialog/requested/LegalHoldRequestedViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/newauthentication/login/LoginFlowStateHolder.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/newauthentication/login/LoginNavigator.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelFactory.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelFactory.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/authentication/login/SavedStateLoginSavedInputStoreTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/newauthentication/login/LoginFlowStateHolderTest.kt create mode 100644 app/src/test/kotlin/com/wire/android/ui/newauthentication/login/LoginNavigatorTest.kt create mode 100644 core/runtime-kmp/build.gradle.kts create mode 100644 core/runtime-kmp/src/commonMain/kotlin/com/wire/android/runtime/viewmodel/IosViewModel.kt create mode 100644 core/runtime-kmp/src/commonTest/kotlin/com/wire/android/runtime/viewmodel/IosViewModelTest.kt create mode 100644 core/ui-common/src/main/kotlin/com/wire/android/model/ImageAssetViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/movetofolder/MoveToFolderViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/PublicLinkViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/expiration/PublicLinkExpirationScreenViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/publiclink/settings/password/PublicLinkPasswordScreenViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/rename/RenameNodeViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/search/SearchScreenViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsViewModelFactory.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/versioning/VersionHistoryViewModelFactory.kt create mode 100644 features/meetings/src/main/java/com/wire/android/feature/meetings/ui/list/MeetingListViewModelFactory.kt create mode 100644 features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsMenuViewModelFactory.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fe22271526f..3fe03d4ad2c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,7 +29,6 @@ plugins { id(libs.plugins.wire.hilt.get().pluginId) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) - alias(libs.plugins.metro) alias(libs.plugins.compose.compiler) id(libs.plugins.aboutLibraries.get().pluginId) 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/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/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/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/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/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/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/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/summary/CreateAccountSummaryViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryViewModelFactory.kt new file mode 100644 index 00000000000..bfdd28b3700 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/summary/CreateAccountSummaryViewModelFactory.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.authentication.create.summary + +import dev.zacsweers.metro.Inject + +@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/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/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/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/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/LoginSavedInputStoreModule.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginSavedInputStoreModule.kt new file mode 100644 index 00000000000..b08f58ba084 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginSavedInputStoreModule.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.authentication.login + +import androidx.lifecycle.SavedStateHandle +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +object LoginSavedInputStoreModule { + @Provides + fun provideLoginSavedInputStore(savedStateHandle: SavedStateHandle): LoginSavedInputStore = + SavedStateLoginSavedInputStore(savedStateHandle) +} 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 index 8e2fa94a810..93dfb25af61 100644 --- 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 @@ -19,10 +19,6 @@ package com.wire.android.ui.authentication.login import androidx.lifecycle.SavedStateHandle -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent private const val USER_IDENTIFIER_SAVED_STATE_KEY = "user_identifier" private const val SSO_CODE_SAVED_STATE_KEY = "sso_code" @@ -42,11 +38,3 @@ class SavedStateLoginSavedInputStore( savedStateHandle[SSO_CODE_SAVED_STATE_KEY] = value } } - -@Module -@InstallIn(ViewModelComponent::class) -object LoginSavedInputStoreModule { - @Provides - fun provideLoginSavedInputStore(savedStateHandle: SavedStateHandle): LoginSavedInputStore = - SavedStateLoginSavedInputStore(savedStateHandle) -} 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/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/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/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/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/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/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/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/common/banner/SecurityClassificationViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationViewModelFactory.kt new file mode 100644 index 00000000000..c727b4c379a --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/banner/SecurityClassificationViewModelFactory.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.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): SecurityClassificationViewModel = SecurityClassificationViewModelImpl( + coreLogic = coreLogic, + args = args, + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModelFactory.kt new file mode 100644 index 00000000000..b99743434ed --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModelFactory.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.common.bottomsheet.conversation + +import androidx.work.WorkManager +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.connection.BlockUserUseCase +import com.wire.kalium.logic.feature.connection.UnblockUserUseCase +import com.wire.kalium.logic.feature.conversation.CheckConversationLeaveConditionsUseCase +import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase +import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase +import com.wire.kalium.logic.feature.conversation.delete.MarkConversationAsDeletedLocallyUseCase +import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFolderUseCase +import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +@Suppress("LongParameterList") +class ConversationOptionsMenuViewModelFactory( + @CurrentAccount private val currentAccount: UserId, + private val observeConversationDetails: ObserveConversationDetailsUseCase, + private val observeSelfUser: ObserveSelfUserUseCase, + private val addConversationToFavorites: AddConversationToFavoritesUseCase, + private val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase, + private val removeConversationFromFolder: RemoveConversationFromFolderUseCase, + private val updateConversationArchivedStatus: UpdateConversationArchivedStatusUseCase, + private val updateConversationMutedStatus: UpdateConversationMutedStatusUseCase, + private val deleteTeamConversation: DeleteTeamConversationUseCase, + private val markConversationAsDeletedLocally: MarkConversationAsDeletedLocallyUseCase, + private val leaveConversation: LeaveConversationUseCase, + private val checkConversationLeaveConditions: CheckConversationLeaveConditionsUseCase, + private val blockUser: BlockUserUseCase, + private val unblockUser: UnblockUserUseCase, + private val clearConversationContent: ClearConversationContentUseCase, + private val workManager: WorkManager, + private val dispatchers: DispatcherProvider, +) { + fun create(): ConversationOptionsMenuViewModel = ConversationOptionsMenuViewModelImpl( + currentAccount = currentAccount, + observeConversationDetails = observeConversationDetails, + observeSelfUser = observeSelfUser, + addConversationToFavorites = addConversationToFavorites, + removeConversationFromFavorites = removeConversationFromFavorites, + removeConversationFromFolder = removeConversationFromFolder, + updateConversationArchivedStatus = updateConversationArchivedStatus, + updateConversationMutedStatus = updateConversationMutedStatus, + deleteTeamConversation = deleteTeamConversation, + markConversationAsDeletedLocally = markConversationAsDeletedLocally, + leaveConversation = leaveConversation, + checkConversationLeaveConditions = checkConversationLeaveConditions, + blockUser = blockUser, + unblockUser = unblockUser, + clearConversationContent = clearConversationContent, + workManager = workManager, + dispatchers = dispatchers, + ) +} 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/ConnectionActionButtonViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelFactory.kt new file mode 100644 index 00000000000..81fa3666801 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelFactory.kt @@ -0,0 +1,50 @@ +/* + * 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 = 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/AssetLocalPathViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/AssetLocalPathViewModelFactory.kt new file mode 100644 index 00000000000..cd10811fd1d --- /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 +internal 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/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/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/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/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/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/SearchAppsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelFactory.kt new file mode 100644 index 00000000000..f4f23dd80e8 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelFactory.kt @@ -0,0 +1,50 @@ +/* + * 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.user.ObserveSelfUserUseCase +import dev.zacsweers.metro.Inject + +@Inject +class SearchAppsViewModelFactory( + private val getAllServices: ObserveAllServicesUseCase, + 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, + 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/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/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/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/conversationslist/ConversationListCallViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListCallViewModelFactory.kt new file mode 100644 index 00000000000..c0d355848ee --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListCallViewModelFactory.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.conversationslist + +import com.wire.android.di.CurrentAccount +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 dev.zacsweers.metro.Inject + +@Inject +class ConversationListCallViewModelFactory( + @CurrentAccount private val currentAccount: UserId, + private val answerCall: AnswerCallUseCase, + private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, + private val endCall: EndCallUseCase, +) { + fun create(): ConversationListCallViewModelImpl = ConversationListCallViewModelImpl( + currentAccount = currentAccount, + answerCall = answerCall, + observeEstablishedCalls = observeEstablishedCalls, + endCall = endCall, + ) +} 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/ServiceDetailsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelFactory.kt new file mode 100644 index 00000000000..87dce8df173 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/service/ServiceDetailsViewModelFactory.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.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, + private val serviceDetailsNavArgsProvider: ServiceDetailsNavArgsProvider, +) { + fun create(): 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, + serviceDetailsNavArgsProvider = serviceDetailsNavArgsProvider, + ) +} 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/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/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/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/KmpLibraryConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/KmpLibraryConventionPlugin.kt index 8bf517c0d17..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 { 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/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/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/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/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/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/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/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..5d69a7ffcb0 --- /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 +internal class PublicLinkExpirationScreenViewModelFactory( + private val setExpiration: SetPublicLinkExpirationUseCase, +) { + 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/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..250607ef96a --- /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 +internal class PublicLinkPasswordScreenViewModelFactory( + private val generateRandomPassword: RandomPassword, + private val createPassword: CreatePublicLinkPasswordUseCase, + private val updatePassword: UpdatePublicLinkPasswordUseCase, + private val getPublicLinkPassword: GetPublicLinkPasswordUseCase, +) { + 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/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/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/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/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/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/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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 93270e0abf6..4bc1f7a3433 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -165,6 +165,7 @@ googleGms-gradlePlugin = { module = "com.google.gms:google-services", version.re 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" } @@ -183,6 +184,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" } diff --git a/wireone-kmp/build.gradle.kts b/wireone-kmp/build.gradle.kts index 860c1e8db9b..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) } From b510356af57e56c1cf918ab20d75e8ca95cd6acc Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 18:13:42 +0200 Subject: [PATCH 07/46] feat: use Metro for initial ViewModel runtime pilot --- .../android/di/metro/MetroViewModelExt.kt | 59 +++++++++++++++++++ .../wire/android/di/metro/WireMetroGraph.kt | 21 +++++++ .../media/preview/ImagesPreviewScreen.kt | 5 +- .../ui/settings/about/AboutThisAppScreen.kt | 6 +- .../android/ui/sharing/ImportMediaScreen.kt | 5 +- 5 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelExt.kt 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..b06090bdf30 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelExt.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.di.metro + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +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 + +val LocalWireMetroGraph = staticCompositionLocalOf { + null +} + +@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 = LocalWireMetroGraph.current + 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/WireMetroGraph.kt b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt index 9e667665bcd..ce62ddd5566 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -17,12 +17,33 @@ */ package com.wire.android.di.metro +import android.content.Context +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.home.conversations.media.CheckAssetRestrictionsViewModelFactory +import dagger.hilt.android.qualifiers.ApplicationContext import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.createGraphFactory abstract class WireMetroScope private constructor() @DependencyGraph(WireMetroScope::class) interface WireMetroGraph { + @DependencyGraph.Factory + fun interface Factory { + fun create(@Provides @ApplicationContext context: Context): WireMetroGraph + } + val checkAssetRestrictionsViewModelFactory: CheckAssetRestrictionsViewModelFactory + val aboutThisAppViewModelFactory: AboutThisAppViewModelFactory + + @Provides + fun provideAboutThisAppInfoProvider( + @ApplicationContext context: Context, + ): AboutThisAppInfoProvider = AndroidAboutThisAppInfoProvider(context) } + +fun createWireMetroGraph(context: Context): WireMetroGraph = + createGraphFactory().create(context.applicationContext) 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 dba08e0f21b..aba5a895a81 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 @@ -52,6 +52,7 @@ 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 @@ -94,7 +95,9 @@ fun ImagesPreviewScreen( hiltViewModel( creationCallback = { factory -> factory.create(args) } ), - checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = hiltViewModel() + checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = metroViewModel { + checkAssetRestrictionsViewModelFactory.create() + } ) { LaunchedEffect(checkAssetRestrictionsViewModel.state) { with(checkAssetRestrictionsViewModel.state) { 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/sharing/ImportMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt index fedf66a69fc..6c5fe0a7034 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 @@ -61,6 +61,7 @@ import com.ramcosta.composedestinations.generated.app.destinations.ConversationS 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 @@ -192,7 +193,9 @@ private fun ImportMediaLoadingContent(navigateBack: () -> Unit) { private fun ImportMediaAuthenticatedContent( navigator: Navigator, isRestrictedInTeam: Boolean, - checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = hiltViewModel(), + checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = metroViewModel { + checkAssetRestrictionsViewModelFactory.create() + }, importMediaViewModel: ImportMediaAuthenticatedViewModel = hiltViewModel(), ) { if (isRestrictedInTeam) { From 2e7117323fb28b23d375c17f08a3babcca1c0e2b Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 18:20:36 +0200 Subject: [PATCH 08/46] refactor: migrate more ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 29 +++++++++++++++ .../summary/CreateAccountSummaryScreen.kt | 9 +++-- .../about/dependencies/DependenciesScreen.kt | 6 ++-- .../settings/about/licenses/LicensesScreen.kt | 6 ++-- .../ui/home/whatsnew/WhatsNewScreen.kt | 6 ++-- .../options/MeetingOptionsModalSheetLayout.kt | 36 +++++++++++++++++-- 6 files changed, 79 insertions(+), 13 deletions(-) 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 index ce62ddd5566..e18cdf7af75 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -18,6 +18,16 @@ package com.wire.android.di.metro import android.content.Context +import com.wire.android.ui.authentication.create.summary.CreateAccountSummaryViewModelFactory +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.whatsnew.AndroidReleaseNotesFeedUrlProvider +import com.wire.android.ui.home.whatsnew.ReleaseNotesFeedUrlProvider +import com.wire.android.ui.home.whatsnew.WhatsNewViewModelFactory import com.wire.android.ui.settings.about.AboutThisAppInfoProvider import com.wire.android.ui.settings.about.AboutThisAppViewModelFactory import com.wire.android.ui.settings.about.AndroidAboutThisAppInfoProvider @@ -38,11 +48,30 @@ interface WireMetroGraph { val checkAssetRestrictionsViewModelFactory: CheckAssetRestrictionsViewModelFactory val aboutThisAppViewModelFactory: AboutThisAppViewModelFactory + val createAccountSummaryViewModelFactory: CreateAccountSummaryViewModelFactory + val whatsNewViewModelFactory: WhatsNewViewModelFactory + val dependenciesViewModelFactory: DependenciesViewModelFactory + val licensesViewModelFactory: LicensesViewModelFactory @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 = 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 de1474c3555..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 com.wire.android.di.wireViewModel +import com.wire.android.di.metro.metroViewModel import com.wire.android.R import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -53,10 +53,9 @@ import com.wire.android.ui.theme.wireTypography fun CreateAccountSummaryScreen( navigator: Navigator, args: CreateAccountSummaryNavArgs, - viewModel: CreateAccountSummaryViewModel = - wireViewModel( - creationCallback = { factory -> factory.create(args) } - ) + viewModel: CreateAccountSummaryViewModel = metroViewModel { + createAccountSummaryViewModelFactory.create(args) + } ) { SummaryContent( state = viewModel.summaryState, 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/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/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/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..ef5f14e2ca8 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 @@ -20,13 +20,19 @@ package com.wire.android.feature.meetings.ui.options import android.annotation.SuppressLint import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment 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.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +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 import com.wire.android.feature.meetings.R import com.wire.android.feature.meetings.model.MeetingItem import com.wire.android.feature.meetings.ui.list.MeetingLeadingIcon @@ -53,7 +59,7 @@ 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) @@ -178,6 +184,32 @@ private fun MutableList.addIf(condition: Boolean, element: E) { if (condition) add(element) } +private class MeetingOptionsMetroFactories { + val meetingOptionsMenuViewModelFactory = MeetingOptionsMenuViewModelFactory() +} + +@Composable +private inline fun metroViewModel( + viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + }, + crossinline create: MeetingOptionsMetroFactories.() -> VM, +): VM { + val factories = remember { MeetingOptionsMetroFactories() } + val factory = remember(factories) { + viewModelFactory { + initializer { + factories.create() + } + } + } + return viewModel( + modelClass = VM::class, + viewModelStoreOwner = viewModelStoreOwner, + factory = factory, + ) +} + @PreviewMultipleThemes @Composable fun PreviewMessageOptionsModalSheetLayout() = WireTheme { From b1261ea38b5c0f9f0e4237d33553a536937a570e Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 18:51:35 +0200 Subject: [PATCH 09/46] refactor: migrate settings and sync ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 118 ++++++++++++++++++ .../featureflags/DebugFeatureFlagsScreen.kt | 6 +- .../appearance/CustomizationScreen.kt | 6 +- .../networkSettings/NetworkSettingsScreen.kt | 6 +- .../ui/initialsync/InitialSyncScreen.kt | 6 +- 5 files changed, 134 insertions(+), 8 deletions(-) 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 index e18cdf7af75..e56e2305427 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -18,28 +18,55 @@ package com.wire.android.di.metro import android.content.Context +import com.wire.android.datastore.GlobalDataStore +import com.wire.android.datastore.UserDataStoreProvider +import com.wire.android.di.CurrentAccount +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.emm.ManagedConfigurationsManager import com.wire.android.ui.authentication.create.summary.CreateAccountSummaryViewModelFactory +import com.wire.android.ui.debug.featureflags.DebugFeatureFlagsViewModelFactory 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.whatsnew.AndroidReleaseNotesFeedUrlProvider import com.wire.android.ui.home.whatsnew.ReleaseNotesFeedUrlProvider import com.wire.android.ui.home.whatsnew.WhatsNewViewModelFactory +import com.wire.android.ui.initialsync.InitialSyncViewModelFactory 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.home.conversations.media.CheckAssetRestrictionsViewModelFactory +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.data.user.UserId +import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase +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 com.wire.kalium.logic.sync.ObserveSyncStateUseCase import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent import dev.zacsweers.metro.DependencyGraph import dev.zacsweers.metro.Provides import dev.zacsweers.metro.createGraphFactory +import kotlinx.coroutines.runBlocking abstract class WireMetroScope private constructor() @DependencyGraph(WireMetroScope::class) +@Suppress("TooManyFunctions") interface WireMetroGraph { @DependencyGraph.Factory fun interface Factory { @@ -52,6 +79,82 @@ interface WireMetroGraph { val whatsNewViewModelFactory: WhatsNewViewModelFactory val dependenciesViewModelFactory: DependenciesViewModelFactory val licensesViewModelFactory: LicensesViewModelFactory + val debugFeatureFlagsViewModelFactory: DebugFeatureFlagsViewModelFactory + val customizationViewModelFactory: CustomizationViewModelFactory + val initialSyncViewModelFactory: InitialSyncViewModelFactory + val networkSettingsViewModelFactory: NetworkSettingsViewModelFactory + + @Provides + fun provideWireMetroHiltEntryPoint( + @ApplicationContext context: Context, + ): WireMetroHiltEntryPoint = + EntryPointAccessors.fromApplication(context, WireMetroHiltEntryPoint::class.java) + + @Provides + fun provideGlobalDataStore( + @ApplicationContext context: Context, + ): GlobalDataStore = GlobalDataStore(context) + + @Provides + fun provideCoreLogic(entryPoint: WireMetroHiltEntryPoint): CoreLogic = entryPoint.coreLogic() + + @Provides + fun provideUserDataStoreProvider(entryPoint: WireMetroHiltEntryPoint): UserDataStoreProvider = + entryPoint.userDataStoreProvider() + + @Provides + fun provideDispatchers(entryPoint: WireMetroHiltEntryPoint): DispatcherProvider = entryPoint.dispatcherProvider() + + @Provides + fun provideAutomatedLoginManager(entryPoint: WireMetroHiltEntryPoint): AutomatedLoginManager = + entryPoint.automatedLoginManager() + + @Provides + fun provideManagedConfigurationsManager(entryPoint: WireMetroHiltEntryPoint): ManagedConfigurationsManager = + entryPoint.managedConfigurationsManager() + + @CurrentAccount + @Provides + fun provideCurrentSession(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( + coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ObserveSyncStateUseCase = + coreLogic.getSessionScope(currentAccount).observeSyncState + + @Provides + fun provideGetFeatureConfigUseCase(coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId): GetFeatureConfigUseCase = + coreLogic.getSessionScope(currentAccount).debug.getFeatureConfig + + @Provides + fun provideNetworkSettingsDefaultsProvider( + @ApplicationContext context: Context, + ): NetworkSettingsDefaultsProvider = AndroidNetworkSettingsDefaultsProvider(context) + + @Provides + fun provideCurrentSessionUseCase(coreLogic: CoreLogic): CurrentSessionUseCase = + coreLogic.getGlobalScope().session.currentSession + + @Provides + fun provideObservePersistentWebSocketConnectionStatusUseCase( + coreLogic: CoreLogic, + ): ObservePersistentWebSocketConnectionStatusUseCase = + coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus + + @Provides + fun providePersistPersistentWebSocketConnectionStatusUseCase( + coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): PersistPersistentWebSocketConnectionStatusUseCase = + coreLogic.getSessionScope(currentAccount).persistPersistentWebSocketConnectionStatus @Provides fun provideAboutThisAppInfoProvider( @@ -76,3 +179,18 @@ interface WireMetroGraph { fun createWireMetroGraph(context: Context): WireMetroGraph = createGraphFactory().create(context.applicationContext) + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface WireMetroHiltEntryPoint { + @KaliumCoreLogic + fun coreLogic(): CoreLogic + + fun userDataStoreProvider(): UserDataStoreProvider + + fun dispatcherProvider(): DispatcherProvider + + fun automatedLoginManager(): AutomatedLoginManager + + fun managedConfigurationsManager(): ManagedConfigurationsManager +} 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/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/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/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 From 23fa46a18af2f0f8213a03afba1cd52cfa13ded9 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 19:06:58 +0200 Subject: [PATCH 10/46] refactor: migrate profile and device ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 147 +++++++++++++++++- .../ui/e2eiEnrollment/GetE2EICertificateUI.kt | 6 +- .../ui/home/settings/SettingsScreen.kt | 6 +- .../ui/settings/devices/SelfDevicesScreen.kt | 6 +- .../userprofile/avatarpicker/AvatarPicker.kt | 6 +- 5 files changed, 156 insertions(+), 15 deletions(-) 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 index e56e2305427..072cb206ef3 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -19,12 +19,14 @@ package com.wire.android.di.metro import android.content.Context import com.wire.android.datastore.GlobalDataStore +import com.wire.android.datastore.UserDataStore import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.CurrentAccount import com.wire.android.di.KaliumCoreLogic import com.wire.android.emm.ManagedConfigurationsManager import com.wire.android.ui.authentication.create.summary.CreateAccountSummaryViewModelFactory import com.wire.android.ui.debug.featureflags.DebugFeatureFlagsViewModelFactory +import com.wire.android.ui.e2eiEnrollment.GetE2EICertificateViewModelFactory 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 @@ -35,6 +37,7 @@ import com.wire.android.ui.home.settings.appsettings.networkSettings.AndroidNetw 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.whatsnew.AndroidReleaseNotesFeedUrlProvider import com.wire.android.ui.home.whatsnew.ReleaseNotesFeedUrlProvider import com.wire.android.ui.home.whatsnew.WhatsNewViewModelFactory @@ -42,14 +45,31 @@ import com.wire.android.ui.initialsync.InitialSyncViewModelFactory 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.SelfDevicesViewModelFactory import com.wire.android.ui.home.conversations.media.CheckAssetRestrictionsViewModelFactory +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.util.AvatarImageManager 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.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.asset.GetAvatarAssetUseCase +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.debug.GetFeatureConfigUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.GetUserMlsClientIdentitiesUseCase +import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase +import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +import com.wire.kalium.logic.feature.user.UploadUserAvatarUseCase +import com.wire.kalium.logic.feature.user.UserScope import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import com.wire.kalium.logic.feature.user.webSocketStatus.PersistPersistentWebSocketConnectionStatusUseCase import com.wire.kalium.logic.sync.ObserveSyncStateUseCase @@ -83,6 +103,15 @@ interface WireMetroGraph { val customizationViewModelFactory: CustomizationViewModelFactory val initialSyncViewModelFactory: InitialSyncViewModelFactory val networkSettingsViewModelFactory: NetworkSettingsViewModelFactory + val getE2EICertificateViewModelFactory: GetE2EICertificateViewModelFactory + val settingsViewModelFactory: SettingsViewModelFactory + val selfDevicesViewModelFactory: SelfDevicesViewModelFactory + val avatarPickerViewModelFactory: AvatarPickerViewModelFactory + + val dispatcherProvider: DispatcherProvider + + @get:CurrentAccount + val currentAccount: UserId @Provides fun provideWireMetroHiltEntryPoint( @@ -96,12 +125,20 @@ interface WireMetroGraph { ): GlobalDataStore = GlobalDataStore(context) @Provides - fun provideCoreLogic(entryPoint: WireMetroHiltEntryPoint): CoreLogic = entryPoint.coreLogic() + @KaliumCoreLogic + fun provideKaliumCoreLogic(entryPoint: WireMetroHiltEntryPoint): CoreLogic = entryPoint.coreLogic() @Provides fun provideUserDataStoreProvider(entryPoint: WireMetroHiltEntryPoint): UserDataStoreProvider = entryPoint.userDataStoreProvider() + @Provides + fun provideCurrentAccountUserDataStore( + @CurrentAccount currentAccount: UserId, + userDataStoreProvider: UserDataStoreProvider, + ): UserDataStore = + userDataStoreProvider.getOrCreate(currentAccount) + @Provides fun provideDispatchers(entryPoint: WireMetroHiltEntryPoint): DispatcherProvider = entryPoint.dispatcherProvider() @@ -115,7 +152,7 @@ interface WireMetroGraph { @CurrentAccount @Provides - fun provideCurrentSession(coreLogic: CoreLogic): UserId = + fun provideCurrentSession(@KaliumCoreLogic coreLogic: CoreLogic): UserId = runBlocking { when (val result = coreLogic.getGlobalScope().session.currentSession()) { is CurrentSessionResult.Success -> result.accountInfo.userId @@ -125,33 +162,129 @@ interface WireMetroGraph { @Provides fun provideObserveSyncStateUseCase( - coreLogic: CoreLogic, + @KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId, ): ObserveSyncStateUseCase = coreLogic.getSessionScope(currentAccount).observeSyncState @Provides - fun provideGetFeatureConfigUseCase(coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId): GetFeatureConfigUseCase = + fun provideUserScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): UserScope = + coreLogic.getSessionScope(currentAccount).users + + @Provides + fun provideGetAvatarAssetUseCase(userScope: UserScope): GetAvatarAssetUseCase = + userScope.getPublicAsset + + @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 provideAvatarImageGateway( + avatarImageManager: AvatarImageManager, + dispatchers: DispatcherProvider, + @ApplicationContext context: Context, + ): AvatarImageGateway = + AndroidAvatarImageGateway( + avatarImageManager = avatarImageManager, + dispatchers = dispatchers, + appContext = context, + ) + + @Provides + fun provideGetFeatureConfigUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): GetFeatureConfigUseCase = coreLogic.getSessionScope(currentAccount).debug.getFeatureConfig + @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 provideFetchSelfClientsFromRemoteUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): FetchSelfClientsFromRemoteUseCase = + coreLogic.getSessionScope(currentAccount).client.fetchSelfClients + + @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 provideNetworkSettingsDefaultsProvider( @ApplicationContext context: Context, ): NetworkSettingsDefaultsProvider = AndroidNetworkSettingsDefaultsProvider(context) @Provides - fun provideCurrentSessionUseCase(coreLogic: CoreLogic): CurrentSessionUseCase = + fun provideCurrentSessionUseCase(@KaliumCoreLogic coreLogic: CoreLogic): CurrentSessionUseCase = coreLogic.getGlobalScope().session.currentSession @Provides fun provideObservePersistentWebSocketConnectionStatusUseCase( - coreLogic: CoreLogic, + @KaliumCoreLogic coreLogic: CoreLogic, ): ObservePersistentWebSocketConnectionStatusUseCase = coreLogic.getGlobalScope().observePersistentWebSocketConnectionStatus @Provides fun providePersistPersistentWebSocketConnectionStatusUseCase( - coreLogic: CoreLogic, + @KaliumCoreLogic coreLogic: CoreLogic, @CurrentAccount currentAccount: UserId, ): PersistPersistentWebSocketConnectionStatusUseCase = coreLogic.getSessionScope(currentAccount).persistPersistentWebSocketConnectionStatus 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/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/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/userprofile/avatarpicker/AvatarPicker.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPicker.kt index 7e1c2e1c8b1..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() From 226641a9f786dc1da8d2cee368d3af37d057bf9f Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 19:26:27 +0200 Subject: [PATCH 11/46] refactor: migrate account settings ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 70 +++++++++++++++++++ .../ConversationCryptoStatsScreen.kt | 6 +- .../folder/NewConversationFolderScreen.kt | 6 +- .../account/color/ChangeUserColorScreen.kt | 6 +- .../displayname/ChangeDisplayNameScreen.kt | 4 +- .../email/updateEmail/ChangeEmailScreen.kt | 4 +- .../account/handle/ChangeHandleScreen.kt | 4 +- 7 files changed, 87 insertions(+), 13 deletions(-) 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 index 072cb206ef3..73e20d8c13f 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -25,8 +25,10 @@ import com.wire.android.di.CurrentAccount import com.wire.android.di.KaliumCoreLogic import com.wire.android.emm.ManagedConfigurationsManager import com.wire.android.ui.authentication.create.summary.CreateAccountSummaryViewModelFactory +import com.wire.android.ui.debug.cryptostats.ConversationCryptoStatsViewModelFactory import com.wire.android.ui.debug.featureflags.DebugFeatureFlagsViewModelFactory import com.wire.android.ui.e2eiEnrollment.GetE2EICertificateViewModelFactory +import com.wire.android.ui.home.conversations.folder.NewFolderViewModelFactory 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 @@ -38,6 +40,10 @@ import com.wire.android.ui.home.settings.appsettings.networkSettings.NetworkSett 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.color.ChangeUserColorViewModelFactory +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.handle.ChangeHandleViewModelFactory import com.wire.android.ui.home.whatsnew.AndroidReleaseNotesFeedUrlProvider import com.wire.android.ui.home.whatsnew.ReleaseNotesFeedUrlProvider import com.wire.android.ui.home.whatsnew.WhatsNewViewModelFactory @@ -58,16 +64,26 @@ 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.asset.GetAvatarAssetUseCase +import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase 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.conversation.ConversationScope +import com.wire.kalium.logic.feature.conversation.folder.CreateConversationFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase +import com.wire.kalium.logic.feature.debug.GetConversationCryptoStatsUseCase import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserMlsClientIdentitiesUseCase import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase +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.UploadUserAvatarUseCase import com.wire.kalium.logic.feature.user.UserScope import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase @@ -99,6 +115,7 @@ interface WireMetroGraph { val whatsNewViewModelFactory: WhatsNewViewModelFactory val dependenciesViewModelFactory: DependenciesViewModelFactory val licensesViewModelFactory: LicensesViewModelFactory + val conversationCryptoStatsViewModelFactory: ConversationCryptoStatsViewModelFactory val debugFeatureFlagsViewModelFactory: DebugFeatureFlagsViewModelFactory val customizationViewModelFactory: CustomizationViewModelFactory val initialSyncViewModelFactory: InitialSyncViewModelFactory @@ -107,6 +124,11 @@ interface WireMetroGraph { val settingsViewModelFactory: SettingsViewModelFactory val selfDevicesViewModelFactory: SelfDevicesViewModelFactory val avatarPickerViewModelFactory: AvatarPickerViewModelFactory + val changeUserColorViewModelFactory: ChangeUserColorViewModelFactory + val changeEmailViewModelFactory: ChangeEmailViewModelFactory + val changeDisplayNameViewModelFactory: ChangeDisplayNameViewModelFactory + val changeHandleViewModelFactory: ChangeHandleViewModelFactory + val newFolderViewModelFactory: NewFolderViewModelFactory val dispatcherProvider: DispatcherProvider @@ -221,6 +243,13 @@ interface WireMetroGraph { ): GetFeatureConfigUseCase = coreLogic.getSessionScope(currentAccount).debug.getFeatureConfig + @Provides + fun provideGetConversationCryptoStatsUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): GetConversationCryptoStatsUseCase = + coreLogic.getSessionScope(currentAccount).debug.getConversationCryptoStats + @Provides fun provideObserveIsAppLockEditableUseCase(@KaliumCoreLogic coreLogic: CoreLogic): ObserveIsAppLockEditableUseCase = coreLogic.getGlobalScope().observeIsAppLockEditableUseCase @@ -232,6 +261,32 @@ interface WireMetroGraph { ): ObserveSelfUserUseCase = coreLogic.getSessionScope(currentAccount).users.observeSelfUser + @Provides + fun provideGetSelfUserUseCase(userScope: UserScope): GetSelfUserUseCase = + userScope.getSelfUser + + @Provides + fun provideSetUserHandleUseCase(userScope: UserScope): SetUserHandleUseCase = + userScope.setUserHandle + + @Provides + fun provideValidateUserHandleUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ValidateUserHandleUseCase = + coreLogic.getGlobalScope().validateUserHandleUseCase + + @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 provideFetchSelfClientsFromRemoteUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @@ -267,6 +322,21 @@ interface WireMetroGraph { ): IsE2EIEnabledUseCase = coreLogic.getSessionScope(currentAccount).isE2EIEnabled + @Provides + fun provideConversationScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): ConversationScope = + coreLogic.getSessionScope(currentAccount).conversations + + @Provides + fun provideObserveUserFoldersUseCase(conversationScope: ConversationScope): ObserveUserFoldersUseCase = + conversationScope.observeUserFolders + + @Provides + fun provideCreateConversationFolderUseCase(conversationScope: ConversationScope): CreateConversationFolderUseCase = + conversationScope.createConversationFolder + @Provides fun provideNetworkSettingsDefaultsProvider( @ApplicationContext context: Context, 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/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/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/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/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/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) { From 37624f252d3dec3bedbf9549151792a5424b8ad6 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 19:37:49 +0200 Subject: [PATCH 12/46] refactor: migrate app lock and account ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 142 ++++++++++++++++++ .../ui/e2eiEnrollment/E2EIEnrollmentScreen.kt | 6 +- .../appLock/forgot/ForgotLockCodeScreen.kt | 6 +- .../ui/home/appLock/set/SetLockCodeScreen.kt | 4 +- .../unlock/AppUnlockWithBiometricsScreen.kt | 6 +- .../appLock/unlock/EnterLockCodeScreen.kt | 6 +- .../home/settings/account/MyAccountScreen.kt | 6 +- 7 files changed, 162 insertions(+), 14 deletions(-) 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 index 73e20d8c13f..be1001a0887 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -24,10 +24,20 @@ import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.CurrentAccount import com.wire.android.di.KaliumCoreLogic import com.wire.android.emm.ManagedConfigurationsManager +import com.wire.android.feature.ObserveAppLockConfigUseCase +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.notification.WireNotificationManager import com.wire.android.ui.authentication.create.summary.CreateAccountSummaryViewModelFactory +import com.wire.android.ui.authentication.devices.common.ClearSessionViewModelFactory import com.wire.android.ui.debug.cryptostats.ConversationCryptoStatsViewModelFactory 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.home.appLock.forgot.ForgotLockScreenViewModelFactory +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.folder.NewFolderViewModelFactory import com.wire.android.ui.home.settings.about.dependencies.AndroidDependenciesInfoProvider import com.wire.android.ui.home.settings.about.dependencies.DependenciesInfoProvider @@ -40,7 +50,9 @@ import com.wire.android.ui.home.settings.appsettings.networkSettings.NetworkSett 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.handle.ChangeHandleViewModelFactory @@ -63,9 +75,16 @@ import com.wire.kalium.logic.CoreLogic 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.asset.GetAvatarAssetUseCase +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase +import com.wire.kalium.logic.feature.call.CallsScope +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.FetchSelfClientsFromRemoteUseCase +import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase import com.wire.kalium.logic.feature.conversation.ConversationScope @@ -77,10 +96,19 @@ import com.wire.kalium.logic.feature.e2ei.usecase.GetUserMlsClientIdentitiesUseC import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase 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.GetSessionsUseCase +import com.wire.kalium.logic.feature.team.TeamScope +import com.wire.kalium.logic.feature.user.DeleteAccountUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase 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.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 @@ -120,7 +148,9 @@ interface WireMetroGraph { val customizationViewModelFactory: CustomizationViewModelFactory val initialSyncViewModelFactory: InitialSyncViewModelFactory val networkSettingsViewModelFactory: NetworkSettingsViewModelFactory + val e2EIEnrollmentViewModelFactory: E2EIEnrollmentViewModelFactory val getE2EICertificateViewModelFactory: GetE2EICertificateViewModelFactory + val clearSessionViewModelFactory: ClearSessionViewModelFactory val settingsViewModelFactory: SettingsViewModelFactory val selfDevicesViewModelFactory: SelfDevicesViewModelFactory val avatarPickerViewModelFactory: AvatarPickerViewModelFactory @@ -128,7 +158,13 @@ interface WireMetroGraph { val changeEmailViewModelFactory: ChangeEmailViewModelFactory val changeDisplayNameViewModelFactory: ChangeDisplayNameViewModelFactory val changeHandleViewModelFactory: ChangeHandleViewModelFactory + val myAccountViewModelFactory: MyAccountViewModelFactory + val deleteAccountViewModelFactory: DeleteAccountViewModelFactory + val appUnlockWithBiometricsViewModelFactory: AppUnlockWithBiometricsViewModelFactory + val enterLockScreenViewModelFactory: EnterLockScreenViewModelFactory val newFolderViewModelFactory: NewFolderViewModelFactory + val forgotLockScreenViewModelFactory: ForgotLockScreenViewModelFactory + val setLockScreenViewModelFactory: SetLockScreenViewModelFactory val dispatcherProvider: DispatcherProvider @@ -172,6 +208,18 @@ interface WireMetroGraph { fun provideManagedConfigurationsManager(entryPoint: WireMetroHiltEntryPoint): ManagedConfigurationsManager = entryPoint.managedConfigurationsManager() + @Provides + fun provideLockCodeTimeManager(entryPoint: WireMetroHiltEntryPoint): LockCodeTimeManager = + entryPoint.lockCodeTimeManager() + + @Provides + fun provideWireNotificationManager(entryPoint: WireMetroHiltEntryPoint): WireNotificationManager = + entryPoint.wireNotificationManager() + + @Provides + fun provideAccountSwitchUseCase(entryPoint: WireMetroHiltEntryPoint): AccountSwitchUseCase = + entryPoint.accountSwitchUseCase() + @CurrentAccount @Provides fun provideCurrentSession(@KaliumCoreLogic coreLogic: CoreLogic): UserId = @@ -261,10 +309,30 @@ interface WireMetroGraph { ): ObserveSelfUserUseCase = coreLogic.getSessionScope(currentAccount).users.observeSelfUser + @Provides + fun provideObserveSelfUserWithTeamUseCase(userScope: UserScope): ObserveSelfUserWithTeamUseCase = + userScope.observeSelfUserWithTeam + @Provides fun provideGetSelfUserUseCase(userScope: UserScope): GetSelfUserUseCase = userScope.getSelfUser + @Provides + fun provideSelfServerConfigUseCase(userScope: UserScope): SelfServerConfigUseCase = + userScope.serverLinks + + @Provides + fun provideIsPasswordRequiredUseCase(userScope: UserScope): IsPasswordRequiredUseCase = + userScope.isPasswordRequired + + @Provides + fun provideIsReadOnlyAccountUseCase(userScope: UserScope): IsReadOnlyAccountUseCase = + userScope.isReadOnlyAccount + + @Provides + fun provideDeleteAccountUseCase(userScope: UserScope): DeleteAccountUseCase = + userScope.deleteAccount + @Provides fun provideSetUserHandleUseCase(userScope: UserScope): SetUserHandleUseCase = userScope.setUserHandle @@ -275,6 +343,29 @@ interface WireMetroGraph { ): ValidateUserHandleUseCase = coreLogic.getGlobalScope().validateUserHandleUseCase + @Provides + fun provideValidatePasswordUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ValidatePasswordUseCase = + coreLogic.getGlobalScope().validatePasswordUseCase + + @Provides + fun provideObserveAppLockConfigUseCase( + globalDataStore: GlobalDataStore, + @KaliumCoreLogic coreLogic: CoreLogic, + ): ObserveAppLockConfigUseCase = + ObserveAppLockConfigUseCase( + globalDataStore = globalDataStore, + coreLogic = coreLogic, + ) + + @Provides + fun provideMarkTeamAppLockStatusAsNotifiedUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): MarkTeamAppLockStatusAsNotifiedUseCase = + coreLogic.getSessionScope(currentAccount).markTeamAppLockStatusAsNotified + @Provides fun provideUpdateEmailUseCase(userScope: UserScope): UpdateEmailUseCase = userScope.updateEmail @@ -287,6 +378,21 @@ interface WireMetroGraph { 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 provideFetchSelfClientsFromRemoteUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @@ -322,6 +428,17 @@ interface WireMetroGraph { ): IsE2EIEnabledUseCase = coreLogic.getSessionScope(currentAccount).isE2EIEnabled + @Provides + fun provideTeamScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): TeamScope = + coreLogic.getSessionScope(currentAccount).team + + @Provides + fun provideIsSelfATeamMemberUseCase(teamScope: TeamScope): IsSelfATeamMemberUseCase = + teamScope.isSelfATeamMember + @Provides fun provideConversationScope( @KaliumCoreLogic coreLogic: CoreLogic, @@ -337,6 +454,25 @@ interface WireMetroGraph { fun provideCreateConversationFolderUseCase(conversationScope: ConversationScope): CreateConversationFolderUseCase = conversationScope.createConversationFolder + @Provides + fun provideGetSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): GetSessionsUseCase = + coreLogic.getGlobalScope().session.allSessions + + @Provides + fun provideCallsScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): CallsScope = + coreLogic.getSessionScope(currentAccount).calls + + @Provides + fun provideObserveEstablishedCallsUseCase(callsScope: CallsScope): ObserveEstablishedCallsUseCase = + callsScope.establishedCall + + @Provides + fun provideEndCallUseCase(callsScope: CallsScope): EndCallUseCase = + callsScope.endCall + @Provides fun provideNetworkSettingsDefaultsProvider( @ApplicationContext context: Context, @@ -396,4 +532,10 @@ interface WireMetroHiltEntryPoint { fun automatedLoginManager(): AutomatedLoginManager fun managedConfigurationsManager(): ManagedConfigurationsManager + + fun lockCodeTimeManager(): LockCodeTimeManager + + fun wireNotificationManager(): WireNotificationManager + + fun accountSwitchUseCase(): AccountSwitchUseCase } 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/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/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/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/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/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() From 8539ad498557014051e43bd36226a2e9b34b3658 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 19:47:25 +0200 Subject: [PATCH 13/46] refactor: migrate debug and device ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 150 +++++++++++++++++- .../com/wire/android/ui/debug/DebugScreen.kt | 6 +- .../android/ui/debug/LogManagementScreen.kt | 6 +- .../settings/privacy/PrivacySettingsScreen.kt | 4 +- .../settings/devices/DeviceDetailsScreen.kt | 6 +- .../e2ei/E2eiCertificateDetailsScreen.kt | 11 +- .../ui/userprofile/qr/SelfQRCodeScreen.kt | 6 +- 7 files changed, 168 insertions(+), 21 deletions(-) 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 index be1001a0887..4ca76a7b5a1 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -18,17 +18,22 @@ package com.wire.android.di.metro import android.content.Context +import com.wire.android.BuildConfig import com.wire.android.datastore.GlobalDataStore import com.wire.android.datastore.UserDataStore import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.CurrentAccount import com.wire.android.di.KaliumCoreLogic import com.wire.android.emm.ManagedConfigurationsManager -import com.wire.android.feature.ObserveAppLockConfigUseCase import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.feature.ObserveAppLockConfigUseCase +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.notification.WireNotificationManager import com.wire.android.ui.authentication.create.summary.CreateAccountSummaryViewModelFactory import com.wire.android.ui.authentication.devices.common.ClearSessionViewModelFactory +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.featureflags.DebugFeatureFlagsViewModelFactory import com.wire.android.ui.e2eiEnrollment.E2EIEnrollmentViewModelFactory @@ -56,21 +61,29 @@ import com.wire.android.ui.home.settings.account.deleteAccount.DeleteAccountView 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.handle.ChangeHandleViewModelFactory +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.analytics.AnalyticsConfiguration import com.wire.android.ui.initialsync.InitialSyncViewModelFactory 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.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.qr.AndroidSelfQRCodeAssetRepository +import com.wire.android.ui.userprofile.qr.SelfQRCodeAssetRepository +import com.wire.android.ui.userprofile.qr.SelfQRCodeViewModelFactory import com.wire.android.util.AvatarImageManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.lifecycle.AutomatedLoginManager +import com.wire.android.util.logging.LogFileWriter import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.asset.KaliumFileSystem import com.wire.kalium.logic.data.id.QualifiedIdMapper @@ -83,15 +96,25 @@ import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase import com.wire.kalium.logic.feature.call.CallsScope 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.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.FinalizeMLSClientAfterE2EIEnrollment +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.conversation.ConversationScope import com.wire.kalium.logic.feature.conversation.folder.CreateConversationFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase +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.GetConversationCryptoStatsUseCase import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase +import com.wire.kalium.logic.feature.debug.ObserveDatabaseLoggerStateUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.GetMLSClientIdentityUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserMlsClientIdentitiesUseCase import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult @@ -107,6 +130,7 @@ 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.SetUserHandleUseCase import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase import com.wire.kalium.logic.feature.user.UpdateAccentColorUseCase @@ -114,6 +138,12 @@ import com.wire.kalium.logic.feature.user.UpdateDisplayNameUseCase import com.wire.kalium.logic.feature.user.UpdateEmailUseCase 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.sync.ObserveSyncStateUseCase @@ -143,7 +173,9 @@ interface WireMetroGraph { val whatsNewViewModelFactory: WhatsNewViewModelFactory val dependenciesViewModelFactory: DependenciesViewModelFactory val licensesViewModelFactory: LicensesViewModelFactory + val userDebugViewModelFactory: UserDebugViewModelFactory val conversationCryptoStatsViewModelFactory: ConversationCryptoStatsViewModelFactory + val logManagementViewModelFactory: LogManagementViewModelFactory val debugFeatureFlagsViewModelFactory: DebugFeatureFlagsViewModelFactory val customizationViewModelFactory: CustomizationViewModelFactory val initialSyncViewModelFactory: InitialSyncViewModelFactory @@ -153,6 +185,8 @@ interface WireMetroGraph { val clearSessionViewModelFactory: ClearSessionViewModelFactory val settingsViewModelFactory: SettingsViewModelFactory val selfDevicesViewModelFactory: SelfDevicesViewModelFactory + val deviceDetailsViewModelFactory: DeviceDetailsViewModelFactory + val e2eiCertificateDetailsViewModelFactory: E2eiCertificateDetailsViewModelFactory val avatarPickerViewModelFactory: AvatarPickerViewModelFactory val changeUserColorViewModelFactory: ChangeUserColorViewModelFactory val changeEmailViewModelFactory: ChangeEmailViewModelFactory @@ -165,6 +199,8 @@ interface WireMetroGraph { val newFolderViewModelFactory: NewFolderViewModelFactory val forgotLockScreenViewModelFactory: ForgotLockScreenViewModelFactory val setLockScreenViewModelFactory: SetLockScreenViewModelFactory + val privacySettingsViewModelFactory: PrivacySettingsViewModelFactory + val selfQRCodeViewModelFactory: SelfQRCodeViewModelFactory val dispatcherProvider: DispatcherProvider @@ -216,10 +252,18 @@ interface WireMetroGraph { fun provideWireNotificationManager(entryPoint: WireMetroHiltEntryPoint): WireNotificationManager = entryPoint.wireNotificationManager() + @Provides + fun provideLogFileWriter(entryPoint: WireMetroHiltEntryPoint): LogFileWriter = + entryPoint.logFileWriter() + @Provides fun provideAccountSwitchUseCase(entryPoint: WireMetroHiltEntryPoint): AccountSwitchUseCase = entryPoint.accountSwitchUseCase() + @Provides + fun provideAnonymousAnalyticsManager(): AnonymousAnalyticsManager = + AnonymousAnalyticsManagerImpl + @CurrentAccount @Provides fun provideCurrentSession(@KaliumCoreLogic coreLogic: CoreLogic): UserId = @@ -244,6 +288,20 @@ interface WireMetroGraph { ): 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 provideGetAvatarAssetUseCase(userScope: UserScope): GetAvatarAssetUseCase = userScope.getPublicAsset @@ -284,6 +342,18 @@ interface WireMetroGraph { 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, @@ -298,6 +368,24 @@ interface WireMetroGraph { ): GetConversationCryptoStatsUseCase = coreLogic.getSessionScope(currentAccount).debug.getConversationCryptoStats + @Provides + fun provideBreakSessionUseCase(debugScope: DebugScope): BreakSessionUseCase = + debugScope.breakSession + + @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 @@ -313,14 +401,56 @@ interface WireMetroGraph { fun provideObserveSelfUserWithTeamUseCase(userScope: UserScope): ObserveSelfUserWithTeamUseCase = userScope.observeSelfUserWithTeam + @Provides + fun provideObserveUserInfoUseCase(userScope: UserScope): ObserveUserInfoUseCase = + userScope.observeUserInfo + @Provides fun provideGetSelfUserUseCase(userScope: UserScope): GetSelfUserUseCase = userScope.getSelfUser + @Provides + fun provideGetMLSClientIdentityUseCase(userScope: UserScope): GetMLSClientIdentityUseCase = + userScope.getE2EICertificate + @Provides fun provideSelfServerConfigUseCase(userScope: UserScope): SelfServerConfigUseCase = userScope.serverLinks + @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 @@ -393,6 +523,10 @@ interface WireMetroGraph { ): LogoutUseCase = coreLogic.getSessionScope(currentAccount).logout + @Provides + fun provideDeleteClientUseCase(clientScope: ClientScope): DeleteClientUseCase = + clientScope.deleteClient + @Provides fun provideFetchSelfClientsFromRemoteUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @@ -400,6 +534,18 @@ interface WireMetroGraph { ): FetchSelfClientsFromRemoteUseCase = coreLogic.getSessionScope(currentAccount).client.fetchSelfClients + @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, @@ -537,5 +683,7 @@ interface WireMetroHiltEntryPoint { fun wireNotificationManager(): WireNotificationManager + fun logFileWriter(): LogFileWriter + fun accountSwitchUseCase(): AccountSwitchUseCase } 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 ad082ca03af..380e9d93500 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,7 +41,7 @@ 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.di.metro.metroViewModel import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.di.wireViewModelScoped @@ -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, 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/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/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index 54b56536371..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 @@ -108,9 +108,7 @@ import kotlinx.datetime.Instant fun DeviceDetailsScreen( navigator: Navigator, args: DeviceDetailsNavArgs, - viewModel: DeviceDetailsViewModel = hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + 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/e2ei/E2eiCertificateDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt index 0c4abdaee10..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 @@ -62,10 +62,9 @@ import kotlinx.coroutines.withContext fun E2eiCertificateDetailsScreen( navigator: Navigator, args: E2eiCertificateDetailsScreenNavArgs, - e2eiCertificateDetailsViewModel: E2eiCertificateDetailsViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + e2eiCertificateDetailsViewModel: E2eiCertificateDetailsViewModel = metroViewModel { + e2eiCertificateDetailsViewModelFactory.create(args) + } ) { val snackbarHostState = LocalSnackbarHostState.current val scope = rememberCoroutineScope() 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 01f7ac38020..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 @@ -53,7 +53,7 @@ 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 @@ -82,9 +82,7 @@ import kotlinx.coroutines.launch fun SelfQRCodeScreen( navigator: Navigator, args: SelfQrCodeNavArgs, - viewModel: SelfQRCodeViewModel = hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + viewModel: SelfQRCodeViewModel = metroViewModel { selfQRCodeViewModelFactory.create(args) } ) { if (viewModel.selfQRCodeState.hasError) { navigator.navigateBack() From 9911a4715087783d9942990cff6e299d2a3c490f Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 20:04:09 +0200 Subject: [PATCH 14/46] refactor: migrate profile and media ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 210 ++++++++++++++++++ .../conversation/DebugConversationScreen.kt | 6 +- .../media/preview/ImagesPreviewScreen.kt | 8 +- .../ui/home/gallery/MediaGalleryScreen.kt | 8 +- .../email/verifyEmail/VerifyEmailScreen.kt | 6 +- .../other/OtherUserProfileScreen.kt | 9 +- .../userprofile/self/SelfUserProfileScreen.kt | 3 +- 7 files changed, 227 insertions(+), 23 deletions(-) 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 index 4ca76a7b5a1..068585ca450 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -29,12 +29,15 @@ import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.ObserveAppLockConfigUseCase import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl +import com.wire.android.mapper.OtherAccountMapper +import com.wire.android.mapper.UserTypeMapper import com.wire.android.notification.WireNotificationManager import com.wire.android.ui.authentication.create.summary.CreateAccountSummaryViewModelFactory import com.wire.android.ui.authentication.devices.common.ClearSessionViewModelFactory 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 @@ -43,7 +46,13 @@ 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.folder.NewFolderViewModelFactory +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.usecase.HandleUriAssetUseCase +import com.wire.android.ui.home.gallery.MediaGalleryViewModelFactory 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 @@ -60,6 +69,7 @@ import com.wire.android.ui.home.settings.account.color.ChangeUserColorViewModelF 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.privacy.PrivacySettingsViewModelFactory import com.wire.android.ui.home.whatsnew.AndroidReleaseNotesFeedUrlProvider @@ -77,19 +87,28 @@ import com.wire.android.ui.home.conversations.media.CheckAssetRestrictionsViewMo 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.util.AvatarImageManager +import com.wire.android.util.FileManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.lifecycle.AutomatedLoginManager import com.wire.android.util.logging.LogFileWriter +import com.wire.kalium.cells.CellsScope +import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase +import com.wire.kalium.cells.domain.usecase.GetMessageAttachmentUseCase 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.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.asset.GetAvatarAssetUseCase +import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase import com.wire.kalium.logic.feature.auth.LogoutUseCase import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCase @@ -100,28 +119,45 @@ 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.FinalizeMLSClientAfterE2EIEnrollment +import com.wire.kalium.logic.feature.client.IsProfileQRCodeEnabledUseCase 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.conversation.ConversationScope +import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase +import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase +import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase +import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUseCase import com.wire.kalium.logic.feature.conversation.folder.CreateConversationFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase 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.GetConversationCryptoStatsUseCase +import com.wire.kalium.logic.feature.debug.GetConversationEpochFromCCUseCase import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase import com.wire.kalium.logic.feature.debug.ObserveDatabaseLoggerStateUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetMLSClientIdentityUseCase 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.legalhold.ObserveLegalHoldStateForSelfUserUseCase 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.GetSessionsUseCase import com.wire.kalium.logic.feature.team.TeamScope +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase +import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase +import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase +import com.wire.kalium.logic.feature.message.DeleteMessageUseCase +import com.wire.kalium.logic.feature.message.MessageScope +import com.wire.kalium.logic.feature.team.SyncSelfTeamInfoUseCase import com.wire.kalium.logic.feature.user.DeleteAccountUseCase import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase @@ -131,11 +167,13 @@ 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 @@ -175,6 +213,7 @@ interface WireMetroGraph { val licensesViewModelFactory: LicensesViewModelFactory val userDebugViewModelFactory: UserDebugViewModelFactory val conversationCryptoStatsViewModelFactory: ConversationCryptoStatsViewModelFactory + val debugConversationViewModelFactory: DebugConversationViewModelFactory val logManagementViewModelFactory: LogManagementViewModelFactory val debugFeatureFlagsViewModelFactory: DebugFeatureFlagsViewModelFactory val customizationViewModelFactory: CustomizationViewModelFactory @@ -190,6 +229,7 @@ interface WireMetroGraph { val avatarPickerViewModelFactory: AvatarPickerViewModelFactory val changeUserColorViewModelFactory: ChangeUserColorViewModelFactory val changeEmailViewModelFactory: ChangeEmailViewModelFactory + val verifyEmailViewModelFactory: VerifyEmailViewModelFactory val changeDisplayNameViewModelFactory: ChangeDisplayNameViewModelFactory val changeHandleViewModelFactory: ChangeHandleViewModelFactory val myAccountViewModelFactory: MyAccountViewModelFactory @@ -201,6 +241,10 @@ interface WireMetroGraph { val setLockScreenViewModelFactory: SetLockScreenViewModelFactory val privacySettingsViewModelFactory: PrivacySettingsViewModelFactory val selfQRCodeViewModelFactory: SelfQRCodeViewModelFactory + val mediaGalleryViewModelFactory: MediaGalleryViewModelFactory + val imagesPreviewViewModelFactory: ImagesPreviewViewModelFactory + val otherUserProfileScreenViewModelFactory: OtherUserProfileScreenViewModelFactory + val selfUserProfileViewModelFactory: SelfUserProfileViewModelFactory val dispatcherProvider: DispatcherProvider @@ -264,6 +308,10 @@ interface WireMetroGraph { fun provideAnonymousAnalyticsManager(): AnonymousAnalyticsManager = AnonymousAnalyticsManagerImpl + @Provides + fun provideOtherAccountMapper(): OtherAccountMapper = + OtherAccountMapper() + @CurrentAccount @Provides fun provideCurrentSession(@KaliumCoreLogic coreLogic: CoreLogic): UserId = @@ -368,6 +416,14 @@ interface WireMetroGraph { ): 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 @@ -405,6 +461,40 @@ interface WireMetroGraph { 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 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 provideGetSelfUserUseCase(userScope: UserScope): GetSelfUserUseCase = userScope.getSelfUser @@ -413,6 +503,10 @@ interface WireMetroGraph { fun provideGetMLSClientIdentityUseCase(userScope: UserScope): GetMLSClientIdentityUseCase = userScope.getE2EICertificate + @Provides + fun provideIsOtherUserE2EIVerifiedUseCase(userScope: UserScope): IsOtherUserE2EIVerifiedUseCase = + userScope.getUserE2eiCertificateStatus + @Provides fun provideSelfServerConfigUseCase(userScope: UserScope): SelfServerConfigUseCase = userScope.serverLinks @@ -534,6 +628,10 @@ interface WireMetroGraph { ): FetchSelfClientsFromRemoteUseCase = coreLogic.getSessionScope(currentAccount).client.fetchSelfClients + @Provides + fun provideFetchUsersClientsFromRemoteUseCase(clientScope: ClientScope): FetchUsersClientsFromRemoteUseCase = + clientScope.fetchUsersClients + @Provides fun provideClientFingerPrintUseCase(clientScope: ClientScope): ClientFingerprintUseCase = clientScope.remoteClientFingerPrint @@ -585,6 +683,10 @@ interface WireMetroGraph { fun provideIsSelfATeamMemberUseCase(teamScope: TeamScope): IsSelfATeamMemberUseCase = teamScope.isSelfATeamMember + @Provides + fun provideSyncSelfTeamInfoUseCase(teamScope: TeamScope): SyncSelfTeamInfoUseCase = + teamScope.syncSelfTeamInfoUseCase + @Provides fun provideConversationScope( @KaliumCoreLogic coreLogic: CoreLogic, @@ -592,6 +694,24 @@ interface WireMetroGraph { ): ConversationScope = coreLogic.getSessionScope(currentAccount).conversations + @Provides + fun provideObserveConversationDetailsUseCase(conversationScope: ConversationScope): ObserveConversationDetailsUseCase = + conversationScope.observeConversationDetails + + @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 @@ -600,6 +720,96 @@ interface WireMetroGraph { fun provideCreateConversationFolderUseCase(conversationScope: ConversationScope): CreateConversationFolderUseCase = conversationScope.createConversationFolder + @Provides + fun provideObserveConversationMembersUseCase(conversationScope: ConversationScope): ObserveConversationMembersUseCase = + conversationScope.observeConversationMembers + + @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 provideIsOneToOneConversationCreatedUseCase(conversationScope: ConversationScope): IsOneToOneConversationCreatedUseCase = + conversationScope.isOneToOneConversationCreatedUseCase + + @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 provideCellsScope( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId, + ): CellsScope = + coreLogic.getSessionScope(currentAccount).cells + + @Provides + fun provideGetMessageAttachmentUseCase(cellsScope: CellsScope): GetMessageAttachmentUseCase = + cellsScope.getMessageAttachmentUseCase + + @Provides + fun provideGetCellFileUseCase(cellsScope: CellsScope): GetCellFileUseCase = + cellsScope.getCellFileUseCase + + @Provides + fun provideFileManager(@ApplicationContext context: Context): FileManager = + FileManager(context) + + @Provides + fun provideGetAssetSizeLimitUseCase(userScope: UserScope): GetAssetSizeLimitUseCase = + userScope.getAssetSizeLimit + + @Provides + fun provideHandleUriAssetUseCase( + getAssetSizeLimitUseCase: GetAssetSizeLimitUseCase, + fileManager: FileManager, + kaliumFileSystem: KaliumFileSystem, + dispatchers: DispatcherProvider, + ): HandleUriAssetUseCase = + HandleUriAssetUseCase( + getAssetSizeLimit = getAssetSizeLimitUseCase, + fileManager = fileManager, + kaliumFileSystem = kaliumFileSystem, + dispatchers = dispatchers, + ) + + @Provides + fun provideImagesPreviewAssetImporter( + handleUriAssetUseCase: HandleUriAssetUseCase, + dispatchers: DispatcherProvider, + ): ImagesPreviewAssetImporter = + ImagesPreviewAssetImporterImpl( + handleUriAsset = handleUriAssetUseCase, + dispatchers = dispatchers, + ) + @Provides fun provideGetSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): GetSessionsUseCase = coreLogic.getGlobalScope().session.allSessions 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 273e4063383..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 @@ -68,9 +68,7 @@ fun DebugConversationScreen( args: DebugConversationScreenNavArgs, modifier: Modifier = Modifier, viewModel: DebugConversationViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { debugConversationViewModelFactory.create(args) }, ) { val context = LocalContext.current 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 aba5a895a81..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,7 +49,6 @@ 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 @@ -91,10 +90,9 @@ fun ImagesPreviewScreen( navigator: Navigator, resultNavigator: ResultBackNavigator, args: ImagesPreviewNavArgs, - imagesPreviewViewModel: ImagesPreviewViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + imagesPreviewViewModel: ImagesPreviewViewModel = metroViewModel { + imagesPreviewViewModelFactory.create(args) + }, checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = metroViewModel { checkAssetRestrictionsViewModelFactory.create() } 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 dbaf45ae97a..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 @@ -79,9 +79,9 @@ fun MediaGalleryScreen( resultNavigator: ResultBackNavigator, args: MediaGalleryNavArgs, modifier: Modifier = Modifier, - mediaGalleryViewModel: MediaGalleryViewModel = hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + mediaGalleryViewModel: MediaGalleryViewModel = metroViewModel { + mediaGalleryViewModelFactory.create(args) + } ) { val permissionPermanentlyDeniedDialogState = rememberVisibilityState() 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 6d3662d5b51..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 @@ -58,9 +58,7 @@ import com.wire.android.util.ui.stringWithStyledArgs fun VerifyEmailScreen( navigator: Navigator, args: VerifyEmailNavArgs, - viewModel: VerifyEmailViewModel = hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + 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/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt index 2ebf4a45b6f..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,10 +121,9 @@ fun OtherUserProfileScreen( resultNavigator: ResultBackNavigator, conversationFoldersScreenResultRecipient: ResultRecipient, - viewModel: OtherUserProfileScreenViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(navArgs) } - ) + 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/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index dc2645dd025..429753c2994 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 @@ -54,6 +54,7 @@ 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,7 +118,7 @@ fun SelfUserProfileScreen( navigator: Navigator, loginTypeSelector: LoginTypeSelector, avatarPickerResultRecipient: ResultRecipient, - viewModelSelf: SelfUserProfileViewModel = hiltViewModel(), + viewModelSelf: SelfUserProfileViewModel = metroViewModel { selfUserProfileViewModelFactory.create() }, legalHoldRequestedViewModel: LegalHoldRequestedViewModel = hiltViewModel() ) { val legalHoldSubjectDialogState = rememberVisibilityState() From 87be8192a30c4f91c6608b722efa0c1fa35c3c96 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 20:12:54 +0200 Subject: [PATCH 15/46] refactor: migrate auth entry ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 117 +++++++++++++++++- .../create/email/CreateAccountEmailScreen.kt | 9 +- .../username/CreateAccountUsernameScreen.kt | 6 +- .../devices/register/RegisterDeviceScreen.kt | 6 +- .../RegisterDeviceVerificationCodeScreen.kt | 4 +- .../devices/remove/RemoveDeviceScreen.kt | 6 +- .../RemoveDeviceVerificationCodeScreen.kt | 4 +- .../login/sso/LoginSSOScreen.kt | 8 +- .../authentication/welcome/WelcomeScreen.kt | 8 +- 9 files changed, 142 insertions(+), 26 deletions(-) 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 index 068585ca450..1983875e21c 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -18,11 +18,17 @@ package com.wire.android.di.metro import android.content.Context +import androidx.lifecycle.SavedStateHandle import com.wire.android.BuildConfig +import com.wire.android.analytics.FinalizeRegistrationAnalyticsMetadataUseCase +import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase +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.ClientScopeProvider import com.wire.android.di.CurrentAccount +import com.wire.android.di.DefaultWebSocketEnabledByDefault import com.wire.android.di.KaliumCoreLogic import com.wire.android.emm.ManagedConfigurationsManager import com.wire.android.feature.AccountSwitchUseCase @@ -32,8 +38,17 @@ import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.mapper.OtherAccountMapper import com.wire.android.mapper.UserTypeMapper import com.wire.android.notification.WireNotificationManager +import com.wire.android.ui.authentication.create.email.CreateAccountEmailViewModelFactory 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.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.LogManagementViewModelFactory import com.wire.android.ui.debug.UserDebugViewModelFactory import com.wire.android.ui.debug.cryptostats.ConversationCryptoStatsViewModelFactory @@ -95,12 +110,14 @@ import com.wire.android.ui.userprofile.self.SelfUserProfileViewModelFactory import com.wire.android.util.AvatarImageManager import com.wire.android.util.FileManager 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.logging.LogFileWriter import com.wire.kalium.cells.CellsScope import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetMessageAttachmentUseCase 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 @@ -109,9 +126,12 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase import com.wire.kalium.logic.feature.asset.GetAvatarAssetUseCase import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase +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.verification.RequestSecondFactorVerificationCodeUseCase import com.wire.kalium.logic.feature.call.CallsScope import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase @@ -121,6 +141,7 @@ 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.ObserveClientDetailsUseCase import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase @@ -150,6 +171,7 @@ import com.wire.kalium.logic.feature.legalhold.ObserveLegalHoldStateForSelfUserU 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.GetSessionsUseCase import com.wire.kalium.logic.feature.team.TeamScope import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase @@ -198,7 +220,7 @@ import kotlinx.coroutines.runBlocking abstract class WireMetroScope private constructor() @DependencyGraph(WireMetroScope::class) -@Suppress("TooManyFunctions") +@Suppress("LargeClass", "TooManyFunctions") interface WireMetroGraph { @DependencyGraph.Factory fun interface Factory { @@ -207,7 +229,10 @@ interface WireMetroGraph { val checkAssetRestrictionsViewModelFactory: CheckAssetRestrictionsViewModelFactory val aboutThisAppViewModelFactory: AboutThisAppViewModelFactory + val createAccountEmailViewModelFactory: CreateAccountEmailViewModelFactory val createAccountSummaryViewModelFactory: CreateAccountSummaryViewModelFactory + val createAccountUsernameViewModelFactory: CreateAccountUsernameViewModelFactory + val loginSSOViewModelFactory: LoginSSOViewModelFactory val whatsNewViewModelFactory: WhatsNewViewModelFactory val dependenciesViewModelFactory: DependenciesViewModelFactory val licensesViewModelFactory: LicensesViewModelFactory @@ -222,6 +247,8 @@ interface WireMetroGraph { val e2EIEnrollmentViewModelFactory: E2EIEnrollmentViewModelFactory val getE2EICertificateViewModelFactory: GetE2EICertificateViewModelFactory val clearSessionViewModelFactory: ClearSessionViewModelFactory + val removeDeviceViewModelFactory: RemoveDeviceViewModelFactory + val registerDeviceViewModelFactory: RegisterDeviceViewModelFactory val settingsViewModelFactory: SettingsViewModelFactory val selfDevicesViewModelFactory: SelfDevicesViewModelFactory val deviceDetailsViewModelFactory: DeviceDetailsViewModelFactory @@ -245,6 +272,7 @@ interface WireMetroGraph { val imagesPreviewViewModelFactory: ImagesPreviewViewModelFactory val otherUserProfileScreenViewModelFactory: OtherUserProfileScreenViewModelFactory val selfUserProfileViewModelFactory: SelfUserProfileViewModelFactory + val welcomeViewModelFactory: WelcomeViewModelFactory val dispatcherProvider: DispatcherProvider @@ -288,6 +316,50 @@ interface WireMetroGraph { fun provideManagedConfigurationsManager(entryPoint: WireMetroHiltEntryPoint): ManagedConfigurationsManager = entryPoint.managedConfigurationsManager() + @Provides + fun provideCurrentServerConfig( + managedConfigurationsManager: ManagedConfigurationsManager, + ): ServerConfig.Links = + if (BuildConfig.EMM_SUPPORT_ENABLED) { + managedConfigurationsManager.currentServerConfig + } else { + ServerConfigProvider().getDefaultServerConfig(null) + } + + @Provides + fun provideLoginSavedInputStore(): LoginSavedInputStore = + SavedStateLoginSavedInputStore(SavedStateHandle()) + + @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 provideLockCodeTimeManager(entryPoint: WireMetroHiltEntryPoint): LockCodeTimeManager = entryPoint.lockCodeTimeManager() @@ -515,6 +587,28 @@ interface WireMetroGraph { fun provideAnalyticsConfiguration(): AnalyticsConfiguration = if (BuildConfig.ANALYTICS_ENABLED) AnalyticsConfiguration.Enabled else AnalyticsConfiguration.Disabled + @Provides + fun provideRegistrationAnalyticsManagerUseCase( + globalDataStore: GlobalDataStore, + anonymousAnalyticsManager: AnonymousAnalyticsManager, + ): RegistrationAnalyticsManagerUseCase = + RegistrationAnalyticsManagerUseCase( + globalDataStore = globalDataStore, + anonymousAnalyticsManager = anonymousAnalyticsManager, + ) + + @Provides + fun provideFinalizeRegistrationAnalyticsMetadataUseCase( + globalDataStore: GlobalDataStore, + @CurrentAccount currentAccount: UserId, + @KaliumCoreLogic coreLogic: CoreLogic, + ): FinalizeRegistrationAnalyticsMetadataUseCase = + FinalizeRegistrationAnalyticsMetadataUseCase( + globalDataStore = globalDataStore, + currentAccount = currentAccount, + coreLogic = coreLogic, + ) + @Provides fun provideObserveReadReceiptsEnabledUseCase(userScope: UserScope): ObserveReadReceiptsEnabledUseCase = userScope.observeReadReceiptsEnabled @@ -573,6 +667,12 @@ interface WireMetroGraph { ): ValidatePasswordUseCase = coreLogic.getGlobalScope().validatePasswordUseCase + @Provides + fun provideValidateEmailUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + ): ValidateEmailUseCase = + coreLogic.getGlobalScope().validateEmailUseCase + @Provides fun provideObserveAppLockConfigUseCase( globalDataStore: GlobalDataStore, @@ -621,6 +721,10 @@ interface WireMetroGraph { fun provideDeleteClientUseCase(clientScope: ClientScope): DeleteClientUseCase = clientScope.deleteClient + @Provides + fun provideGetOrRegisterClientUseCase(clientScope: ClientScope): GetOrRegisterClientUseCase = + clientScope.getOrRegister + @Provides fun provideFetchSelfClientsFromRemoteUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @@ -814,6 +918,10 @@ interface WireMetroGraph { fun provideGetSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): GetSessionsUseCase = coreLogic.getGlobalScope().session.allSessions + @Provides + fun provideDoesValidNomadAccountExistUseCase(@KaliumCoreLogic coreLogic: CoreLogic): DoesValidNomadAccountExistUseCase = + coreLogic.getGlobalScope().doesValidNomadAccountExist + @Provides fun provideCallsScope( @KaliumCoreLogic coreLogic: CoreLogic, @@ -838,6 +946,13 @@ interface WireMetroGraph { 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, 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 1a48d0ab390..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 com.wire.android.di.wireViewModel 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,10 +86,9 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun CreateAccountEmailScreen( navigator: Navigator, args: CreateAccountNavArgs, - createAccountEmailViewModel: CreateAccountEmailViewModel = - wireViewModel( - creationCallback = { factory -> factory.create(args) } - ) + 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/username/CreateAccountUsernameScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/username/CreateAccountUsernameScreen.kt index bcaeb71715e..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 com.wire.android.di.wireViewModel 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 = wireViewModel() + viewModel: CreateAccountUsernameViewModel = metroViewModel { + createAccountUsernameViewModelFactory.create() + } ) { UsernameContent( textState = viewModel.textState, 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 1a5e69895b0..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 com.wire.android.di.wireViewModel 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 = wireViewModel(), - clearSessionViewModel: ClearSessionViewModel = wireViewModel(), + 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 dbb18412692..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 com.wire.android.di.wireViewModel +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 = wireViewModel() + viewModel: RegisterDeviceViewModel = metroViewModel { registerDeviceViewModelFactory.create() } ) = VerificationCodeScreenContent( viewModel.secondFactorVerificationCodeTextState, viewModel.secondFactorVerificationCodeState, 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 44c3b3e6f1f..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 com.wire.android.di.wireViewModel +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 = wireViewModel(), - clearSessionViewModel: ClearSessionViewModel = wireViewModel(), + 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 d11fa642ed6..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 com.wire.android.di.wireViewModel +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.authentication.verificationcode.VerificationCodeScreenContent @Composable fun RemoveDeviceVerificationCodeScreen( - viewModel: RemoveDeviceViewModel = wireViewModel() + viewModel: RemoveDeviceViewModel = metroViewModel { removeDeviceViewModelFactory.create() } ) = VerificationCodeScreenContent( viewModel.secondFactorVerificationCodeTextState, viewModel.secondFactorVerificationCodeState, 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 97b7b7eb6c3..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 @@ -41,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 com.wire.android.di.wireViewModel 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 @@ -66,9 +66,9 @@ fun LoginSSOScreen( loginNavArgs: com.wire.android.ui.authentication.login.LoginNavArgs, ssoLoginResult: DeepLinkResult.SSOLogin?, ssoCodeAutoLogin: com.wire.android.ui.authentication.login.SSOCodeAutoLogin?, - loginSSOViewModel: LoginSSOViewModel = wireViewModel( - creationCallback = { factory -> factory.create(loginNavArgs) } - ), + loginSSOViewModel: LoginSSOViewModel = metroViewModel { + loginSSOViewModelFactory.create(loginNavArgs) + }, scrollState: ScrollState = rememberScrollState() ) { val scope = rememberCoroutineScope() 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 a6300ce4edc..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 com.wire.android.di.wireViewModel 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 @@ -114,9 +114,9 @@ import kotlinx.coroutines.flow.scan fun WelcomeScreen( navigator: Navigator, args: WelcomeNavArgs, - viewModel: WelcomeViewModel = wireViewModel( - creationCallback = { factory -> factory.create(args) } - ) + viewModel: WelcomeViewModel = metroViewModel { + welcomeViewModelFactory.create(args) + } ) { WelcomeContent( viewModel.state.isThereActiveSession, From e464cd92a60de6fe2f5f5e8120f84acbe6e1af3e Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 20:18:28 +0200 Subject: [PATCH 16/46] refactor: migrate registration ViewModels to Metro --- .../com/wire/android/di/metro/WireMetroGraph.kt | 12 ++++++++++++ .../create/code/CreateAccountCodeScreen.kt | 9 ++++----- .../create/details/CreateAccountDetailsScreen.kt | 9 ++++----- .../CreatePersonalAccountOverviewScreen.kt | 16 +++++++--------- .../code/CreateAccountVerificationCodeScreen.kt | 9 ++++----- .../details/CreateAccountDataDetailScreen.kt | 9 ++++----- .../selector/CreateAccountSelectorScreen.kt | 7 ++----- 7 files changed, 37 insertions(+), 34 deletions(-) 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 index 1983875e21c..177937603fc 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -38,7 +38,10 @@ import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.mapper.OtherAccountMapper import com.wire.android.mapper.UserTypeMapper import com.wire.android.notification.WireNotificationManager +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 @@ -92,6 +95,9 @@ import com.wire.android.ui.home.whatsnew.ReleaseNotesFeedUrlProvider import com.wire.android.ui.home.whatsnew.WhatsNewViewModelFactory import com.wire.android.ui.analytics.AnalyticsConfiguration 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 @@ -229,9 +235,15 @@ interface 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 loginSSOViewModelFactory: LoginSSOViewModelFactory val whatsNewViewModelFactory: WhatsNewViewModelFactory val dependenciesViewModelFactory: DependenciesViewModelFactory 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 eb6952e0759..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 com.wire.android.di.wireViewModel 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 @@ -76,10 +76,9 @@ import kotlinx.coroutines.job fun CreateAccountCodeScreen( navigator: Navigator, args: CreateAccountNavArgs, - createAccountCodeViewModel: CreateAccountCodeViewModel = - wireViewModel( - creationCallback = { factory -> factory.create(args) } - ) + 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/details/CreateAccountDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsScreen.kt index a228b2b5dc0..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 com.wire.android.di.wireViewModel 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 @@ -75,10 +75,9 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun CreateAccountDetailsScreen( navigator: Navigator, args: CreateAccountNavArgs, - createAccountDetailsViewModel: CreateAccountDetailsViewModel = - wireViewModel( - creationCallback = { factory -> factory.create(args) } - ) + 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/overview/CreatePersonalAccountOverviewScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreatePersonalAccountOverviewScreen.kt index bf28883738b..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 com.wire.android.di.wireViewModel 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 @@ -65,10 +65,9 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun CreatePersonalAccountOverviewScreen( navigator: Navigator, args: CreateAccountOverviewNavArgs, - viewModel: CreateAccountOverviewViewModel = - wireViewModel( - creationCallback = { factory -> factory.create(args) } - ) + viewModel: CreateAccountOverviewViewModel = metroViewModel { + createAccountOverviewViewModelFactory.create(args) + } ) { CreateAccountOverviewScreen(navigator, CreateAccountFlowType.CreatePersonalAccount, viewModel) } @@ -78,10 +77,9 @@ fun CreatePersonalAccountOverviewScreen( fun CreateTeamAccountOverviewScreen( navigator: Navigator, args: CreateAccountOverviewNavArgs, - viewModel: CreateAccountOverviewViewModel = - wireViewModel( - creationCallback = { factory -> factory.create(args) } - ) + viewModel: CreateAccountOverviewViewModel = metroViewModel { + createAccountOverviewViewModelFactory.create(args) + } ) { CreateAccountOverviewScreen(navigator, CreateAccountFlowType.CreateTeam, viewModel) } 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 6d4c1e28b3d..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 com.wire.android.di.wireViewModel 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 @@ -81,10 +81,9 @@ import kotlinx.coroutines.job fun CreateAccountVerificationCodeScreen( navigator: Navigator, args: CreateAccountDataNavArgs, - createAccountCodeVerificationViewModel: CreateAccountVerificationCodeViewModel = - wireViewModel( - creationCallback = { factory -> factory.create(args) } - ) + createAccountCodeVerificationViewModel: CreateAccountVerificationCodeViewModel = metroViewModel { + createAccountVerificationCodeViewModelFactory.create(args) + } ) { with(createAccountCodeVerificationViewModel) { fun navigateToUsernameScreen() = navigator.navigate( 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 6b1eaf7fd4c..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 com.wire.android.di.wireViewModel 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 @@ -96,10 +96,9 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun CreateAccountDataDetailScreen( navigator: Navigator, args: CreateAccountDataNavArgs, - createAccountDataDetailViewModel: CreateAccountDataDetailViewModel = - wireViewModel( - creationCallback = { factory -> factory.create(args) } - ) + createAccountDataDetailViewModel: CreateAccountDataDetailViewModel = metroViewModel { + createAccountDataDetailViewModelFactory.create(args) + } ) { with(createAccountDataDetailViewModel) { fun navigateToCodeScreen() = navigator.navigate( 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 7b871c83bd4..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 com.wire.android.di.wireViewModel 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 @@ -83,10 +83,7 @@ import com.wire.android.ui.common.R as commonR fun CreateAccountSelectorScreen( navigator: Navigator, args: CreateAccountSelectorNavArgs, - viewModel: CreateAccountSelectorViewModel = - wireViewModel( - creationCallback = { factory -> factory.create(args) } - ) + viewModel: CreateAccountSelectorViewModel = metroViewModel { createAccountSelectorViewModelFactory.create(args) } ) { val context = LocalContext.current fun navigateToEmailScreen() { From 1561885077a1765839bc94b2b7953a1d9dce5a4a Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 20:32:25 +0200 Subject: [PATCH 17/46] refactor: migrate login and utility ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 235 ++++++++++++++++++ .../ui/authentication/login/LoginScreen.kt | 12 +- .../settings/backup/BackupAndRestoreScreen.kt | 6 +- .../newauthentication/login/NewLoginScreen.kt | 8 +- .../login/password/NewLoginPasswordScreen.kt | 8 +- .../android/ui/sharing/ImportMediaScreen.kt | 4 +- .../service/ServiceDetailsScreen.kt | 7 +- .../service/ServiceDetailsViewModel.kt | 10 +- .../service/ServiceDetailsViewModelFactory.kt | 5 +- .../service/ServiceDetailsViewModelTest.kt | 14 +- 10 files changed, 273 insertions(+), 36 deletions(-) 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 index 177937603fc..6d65449ddd2 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -49,6 +49,7 @@ import com.wire.android.ui.authentication.devices.remove.RemoveDeviceViewModelFa 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 @@ -69,6 +70,7 @@ import com.wire.android.ui.home.conversations.folder.NewFolderViewModelFactory 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.usecase.GetConversationsFromSearchUseCase import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase import com.wire.android.ui.home.gallery.MediaGalleryViewModelFactory import com.wire.android.ui.home.settings.about.dependencies.AndroidDependenciesInfoProvider @@ -89,6 +91,10 @@ import com.wire.android.ui.home.settings.account.displayname.ChangeDisplayNameVi 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 @@ -105,6 +111,9 @@ 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.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 @@ -113,12 +122,20 @@ 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.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.EMPTY import com.wire.android.util.FileManager 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.logging.LogFileWriter +import com.wire.android.util.ui.AndroidUiTextResolver +import com.wire.android.util.ui.CountdownTimer +import com.wire.android.util.ui.UiTextResolver import com.wire.kalium.cells.CellsScope import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetMessageAttachmentUseCase @@ -137,7 +154,17 @@ 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.ObserveIsAppMemberUseCase +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.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.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase @@ -153,13 +180,19 @@ 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.conversation.AddMemberToConversationUseCase +import com.wire.kalium.logic.feature.conversation.AddServiceToConversationUseCase import com.wire.kalium.logic.feature.conversation.ConversationScope +import com.wire.kalium.logic.feature.conversation.GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUseCase +import com.wire.kalium.logic.feature.conversation.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery 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.ObserveConversationsFromFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.ObserveUserFoldersUseCase import com.wire.kalium.logic.feature.debug.BreakSessionUseCase import com.wire.kalium.logic.feature.debug.ChangeProfilingUseCase @@ -173,6 +206,7 @@ import com.wire.kalium.logic.feature.e2ei.usecase.GetMLSClientIdentityUseCase 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.legalhold.ObserveLegalHoldStateForSelfUserUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.session.CurrentSessionUseCase @@ -182,6 +216,11 @@ import com.wire.kalium.logic.feature.session.GetSessionsUseCase import com.wire.kalium.logic.feature.team.TeamScope import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase import com.wire.kalium.logic.feature.server.GetTeamUrlUseCase +import com.wire.kalium.logic.feature.service.GetServiceByIdUseCase +import com.wire.kalium.logic.feature.service.ObserveIsServiceMemberUseCase +import com.wire.kalium.logic.feature.service.ServiceScope +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.MessageScope @@ -219,6 +258,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Named import dev.zacsweers.metro.Provides import dev.zacsweers.metro.createGraphFactory import kotlinx.coroutines.runBlocking @@ -244,6 +284,7 @@ interface WireMetroGraph { val createAccountUsernameViewModelFactory: CreateAccountUsernameViewModelFactory val createAccountVerificationCodeViewModelFactory: CreateAccountVerificationCodeViewModelFactory val createAccountSelectorViewModelFactory: CreateAccountSelectorViewModelFactory + val loginEmailViewModelFactory: LoginEmailViewModelFactory val loginSSOViewModelFactory: LoginSSOViewModelFactory val whatsNewViewModelFactory: WhatsNewViewModelFactory val dependenciesViewModelFactory: DependenciesViewModelFactory @@ -273,6 +314,7 @@ interface WireMetroGraph { val changeHandleViewModelFactory: ChangeHandleViewModelFactory val myAccountViewModelFactory: MyAccountViewModelFactory val deleteAccountViewModelFactory: DeleteAccountViewModelFactory + val backupAndRestoreViewModelFactory: BackupAndRestoreViewModelFactory val appUnlockWithBiometricsViewModelFactory: AppUnlockWithBiometricsViewModelFactory val enterLockScreenViewModelFactory: EnterLockScreenViewModelFactory val newFolderViewModelFactory: NewFolderViewModelFactory @@ -284,7 +326,10 @@ interface WireMetroGraph { val imagesPreviewViewModelFactory: ImagesPreviewViewModelFactory val otherUserProfileScreenViewModelFactory: OtherUserProfileScreenViewModelFactory val selfUserProfileViewModelFactory: SelfUserProfileViewModelFactory + val serviceDetailsViewModelFactory: ServiceDetailsViewModelFactory val welcomeViewModelFactory: WelcomeViewModelFactory + val newLoginViewModelFactory: NewLoginViewModelFactory + val importMediaAuthenticatedViewModelFactory: ImportMediaAuthenticatedViewModelFactory val dispatcherProvider: DispatcherProvider @@ -338,10 +383,25 @@ interface WireMetroGraph { 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, @@ -685,6 +745,23 @@ interface WireMetroGraph { ): 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 provideObserveAppLockConfigUseCase( globalDataStore: GlobalDataStore, @@ -864,6 +941,48 @@ interface WireMetroGraph { 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 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 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 provideObserveIsAppsAllowedForUsageUseCase(serviceScope: ServiceScope): ObserveIsAppsAllowedForUsageUseCase = + serviceScope.observeIsAppsAllowedForUsage + @Provides fun provideMessageScope( @KaliumCoreLogic coreLogic: CoreLogic, @@ -898,6 +1017,10 @@ interface WireMetroGraph { fun provideFileManager(@ApplicationContext context: Context): FileManager = FileManager(context) + @Provides + fun provideUiTextResolver(@ApplicationContext context: Context): UiTextResolver = + AndroidUiTextResolver(context) + @Provides fun provideGetAssetSizeLimitUseCase(userScope: UserScope): GetAssetSizeLimitUseCase = userScope.getAssetSizeLimit @@ -926,6 +1049,118 @@ interface WireMetroGraph { 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 + @Suppress("LongParameterList") + fun provideGetConversationsFromSearchUseCase( + useCase: GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase, + getFavoriteFolderUseCase: GetFavoriteFolderUseCase, + observeConversationsFromFolder: ObserveConversationsFromFolderUseCase, + userTypeMapper: UserTypeMapper, + dispatchers: DispatcherProvider, + getSelfUser: GetSelfUserUseCase, + uiTextResolver: UiTextResolver, + ): GetConversationsFromSearchUseCase = + GetConversationsFromSearchUseCase( + useCase = useCase, + getFavoriteFolderUseCase = getFavoriteFolderUseCase, + observeConversationsFromFromFolder = observeConversationsFromFolder, + userTypeMapper = userTypeMapper, + dispatchers = dispatchers, + getSelfUser = getSelfUser, + uiTextResolver = uiTextResolver, + ) + + @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 + + @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 provideGetSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): GetSessionsUseCase = coreLogic.getGlobalScope().session.allSessions 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 f1d784a9954..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 com.wire.android.di.wireViewModel 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,9 +86,9 @@ import kotlinx.coroutines.launch fun LoginScreen( navigator: Navigator, loginNavArgs: LoginNavArgs, - loginEmailViewModel: LoginEmailViewModel = wireViewModel( - creationCallback = { factory -> factory.create(loginNavArgs) } - ) + loginEmailViewModel: LoginEmailViewModel = metroViewModel { + loginEmailViewModelFactory.create(loginNavArgs) + } ) { LoginContent( @@ -272,7 +272,9 @@ private fun PreviewLoginScreen() = WireTheme { onSuccess = { _, _ -> }, onRemoveDeviceNeeded = {}, loginNavArgs = LoginNavArgs(), - loginEmailViewModel = wireViewModel(), + loginEmailViewModel = metroViewModel { + loginEmailViewModelFactory.create(LoginNavArgs()) + }, ssoLoginResult = null, ssoCodeAutoLogin = null ) 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 511efee8ac1..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,7 +64,9 @@ import com.wire.android.util.ui.PreviewMultipleThemes @Composable fun BackupAndRestoreScreen( navigator: Navigator, - viewModel: BackupAndRestoreViewModel = hiltViewModel() + viewModel: BackupAndRestoreViewModel = metroViewModel { + backupAndRestoreViewModelFactory.create() + } ) { BackupAndRestoreContent( backUpAndRestoreState = viewModel.state, 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 7780bfe3f07..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 @@ -57,7 +57,7 @@ import com.ramcosta.composedestinations.generated.app.destinations.RemoveDeviceS import com.ramcosta.composedestinations.generated.app.destinations.WelcomeScreenDestination import com.ramcosta.composedestinations.spec.Direction import com.wire.android.R -import com.wire.android.di.wireViewModel +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 @@ -99,9 +99,9 @@ import kotlinx.serialization.json.Json fun NewLoginScreen( navigator: Navigator, navArgs: LoginNavArgs, - viewModel: NewLoginViewModel = wireViewModel( - creationCallback = { factory -> factory.create(navArgs) } - ) + viewModel: NewLoginViewModel = metroViewModel { + newLoginViewModelFactory.create(navArgs, provideLoginSavedInputStore()) + } ) { val context = LocalContext.current val currentKeyboardController by rememberUpdatedState(LocalSoftwareKeyboardController.current) 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 07a7fa1ff2c..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 com.wire.android.di.wireViewModel 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,9 +105,9 @@ import com.wire.kalium.logic.configuration.server.ServerConfig fun NewLoginPasswordScreen( navigator: Navigator, navArgs: LoginNavArgs, - loginEmailViewModel: LoginEmailViewModel = wireViewModel( - creationCallback = { factory -> factory.create(navArgs) } - ) + loginEmailViewModel: LoginEmailViewModel = metroViewModel { + loginEmailViewModelFactory.create(navArgs) + } ) { clearAutofillTree() LoginStateNavigationAndDialogs(loginEmailViewModel, navigator) 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 6c5fe0a7034..d2a6813e511 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 @@ -196,7 +196,9 @@ private fun ImportMediaAuthenticatedContent( checkAssetRestrictionsViewModel: CheckAssetRestrictionsViewModel = metroViewModel { checkAssetRestrictionsViewModelFactory.create() }, - importMediaViewModel: ImportMediaAuthenticatedViewModel = hiltViewModel(), + importMediaViewModel: ImportMediaAuthenticatedViewModel = metroViewModel { + importMediaAuthenticatedViewModelFactory.create() + }, ) { if (isRestrictedInTeam) { ImportMediaRestrictedContent( 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 46b02b1fb82..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 @@ -44,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 @@ -56,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, @@ -73,12 +70,9 @@ class ServiceDetailsViewModel @Inject constructor( private val removeMemberFromConversation: RemoveMemberFromConversationUseCase, private val addServiceToConversation: AddServiceToConversationUseCase, private val addMemberToConversation: AddMemberToConversationUseCase, - serviceDetailsNavArgsProvider: ServiceDetailsNavArgsProvider + private val serviceDetailsNavArgs: ServiceDetailsNavArgs ) : ViewModel() { - private val serviceDetailsNavArgs: ServiceDetailsNavArgs = - serviceDetailsNavArgsProvider.serviceDetailsNavArgs() - 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 index 87dce8df173..d7dac1fa73e 100644 --- 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 @@ -47,9 +47,8 @@ class ServiceDetailsViewModelFactory( private val removeMemberFromConversation: RemoveMemberFromConversationUseCase, private val addServiceToConversation: AddServiceToConversationUseCase, private val addMemberToConversation: AddMemberToConversationUseCase, - private val serviceDetailsNavArgsProvider: ServiceDetailsNavArgsProvider, ) { - fun create(): ServiceDetailsViewModel = ServiceDetailsViewModel( + fun create(args: ServiceDetailsNavArgs): ServiceDetailsViewModel = ServiceDetailsViewModel( dispatchers = dispatchers, selfUserId = selfUserId, getServiceById = getServiceById, @@ -62,6 +61,6 @@ class ServiceDetailsViewModelFactory( removeMemberFromConversation = removeMemberFromConversation, addServiceToConversation = addServiceToConversation, addMemberToConversation = addMemberToConversation, - serviceDetailsNavArgsProvider = serviceDetailsNavArgsProvider, + serviceDetailsNavArgs = args, ) } 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 66a24e862af..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 @@ -53,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 @@ -619,10 +618,11 @@ class ServiceDetailsViewModelTest { @MockK lateinit var addMemberToConversation: AddMemberToConversationUseCase - @MockK - lateinit var serviceDetailsNavArgsProvider: ServiceDetailsNavArgsProvider - private val selfUser = TestUser.SELF_USER + private var serviceDetailsNavArgs = ServiceDetailsNavArgs( + CONVERSATION_ID, + ServiceDetailsNavArgs.Id.BotServiceId(BOT_SERVICE) + ) private val viewModel by lazy { ServiceDetailsViewModel( @@ -638,7 +638,7 @@ class ServiceDetailsViewModelTest { removeMemberFromConversation, addServiceToConversation, addMemberToConversation, - serviceDetailsNavArgsProvider + serviceDetailsNavArgs ) } @@ -657,14 +657,14 @@ class ServiceDetailsViewModelTest { } fun withServiceBot(service: BotService, conversationId: ConversationId? = CONVERSATION_ID) = apply { - every { serviceDetailsNavArgsProvider.serviceDetailsNavArgs() } returns ServiceDetailsNavArgs( + serviceDetailsNavArgs = ServiceDetailsNavArgs( conversationId, ServiceDetailsNavArgs.Id.BotServiceId(service) ) } fun withServiceApp(service: UserId, conversationId: ConversationId? = CONVERSATION_ID) = apply { - every { serviceDetailsNavArgsProvider.serviceDetailsNavArgs() } returns ServiceDetailsNavArgs( + serviceDetailsNavArgs = ServiceDetailsNavArgs( conversationId, ServiceDetailsNavArgs.Id.AppId(service) ) From 56673c5700e89fbe0e7abbb74c43ad14cf479aa8 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 13 May 2026 20:41:31 +0200 Subject: [PATCH 18/46] refactor: migrate common and debug ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 209 ++++++++++++++++++ .../android/ui/common/AddContactButton.kt | 15 +- .../banner/SecurityClassificationBanner.kt | 30 +-- .../banner/SecurityClassificationViewModel.kt | 18 +- .../SecurityClassificationViewModelFactory.kt | 9 +- .../ConversationOptionsMenuViewModel.kt | 8 +- ...ConversationOptionsMenuViewModelFactory.kt | 2 +- .../ConversationOptionsModalSheetLayout.kt | 7 +- .../ConnectionActionButtonViewModelFactory.kt | 23 +- .../wire/android/ui/debug/DebugDataOptions.kt | 6 +- .../ui/debug/DebugDataOptionsViewModel.kt | 6 +- .../com/wire/android/ui/debug/DebugScreen.kt | 5 +- .../debug/ExportObfuscatedCopyFileGateway.kt | 3 +- .../ui/debug/ExportObfuscatedCopyViewModel.kt | 5 +- .../JoinConversationViaCodeViewModel.kt | 5 +- .../JoinConversationViaDeepLinkDialog.kt | 6 +- 16 files changed, 275 insertions(+), 82 deletions(-) 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 index 6d65449ddd2..e8ac50a1c42 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -19,6 +19,7 @@ package com.wire.android.di.metro import android.content.Context import androidx.lifecycle.SavedStateHandle +import androidx.work.WorkManager import com.wire.android.BuildConfig import com.wire.android.analytics.FinalizeRegistrationAnalyticsMetadataUseCase import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase @@ -38,6 +39,8 @@ import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.mapper.OtherAccountMapper import com.wire.android.mapper.UserTypeMapper import com.wire.android.notification.WireNotificationManager +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 @@ -53,6 +56,12 @@ 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 @@ -111,6 +120,8 @@ 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.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 @@ -162,6 +173,7 @@ import com.wire.kalium.logic.feature.app.ObserveIsAppMemberUseCase 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 @@ -182,26 +194,51 @@ import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase 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.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.ClearConversationContentUseCase import com.wire.kalium.logic.feature.conversation.ConversationScope +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.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase +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.delete.MarkConversationAsDeletedLocallyUseCase 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.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.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.GetMLSClientIdentityUseCase import com.wire.kalium.logic.feature.e2ei.usecase.GetUserMlsClientIdentitiesUseCase import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCase @@ -225,7 +262,11 @@ import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import com.wire.kalium.logic.feature.message.DeleteMessageUseCase import com.wire.kalium.logic.feature.message.MessageScope 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.IsE2EIEnabledUseCase import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase @@ -252,6 +293,9 @@ import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicator import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import com.wire.kalium.logic.feature.user.webSocketStatus.PersistPersistentWebSocketConnectionStatusUseCase 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.util.DelicateKaliumApi import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.EntryPoint import dagger.hilt.InstallIn @@ -294,6 +338,8 @@ interface WireMetroGraph { val debugConversationViewModelFactory: DebugConversationViewModelFactory val logManagementViewModelFactory: LogManagementViewModelFactory val debugFeatureFlagsViewModelFactory: DebugFeatureFlagsViewModelFactory + val debugDataOptionsViewModelFactory: DebugDataOptionsViewModelFactory + val exportObfuscatedCopyViewModelFactory: ExportObfuscatedCopyViewModelFactory val customizationViewModelFactory: CustomizationViewModelFactory val initialSyncViewModelFactory: InitialSyncViewModelFactory val networkSettingsViewModelFactory: NetworkSettingsViewModelFactory @@ -330,6 +376,10 @@ interface WireMetroGraph { val welcomeViewModelFactory: WelcomeViewModelFactory val newLoginViewModelFactory: NewLoginViewModelFactory val importMediaAuthenticatedViewModelFactory: ImportMediaAuthenticatedViewModelFactory + val joinConversationViaCodeViewModelFactory: JoinConversationViaCodeViewModelFactory + val securityClassificationViewModelFactory: SecurityClassificationViewModelFactory + val connectionActionButtonViewModelFactory: ConnectionActionButtonViewModelFactory + val conversationOptionsMenuViewModelFactory: ConversationOptionsMenuViewModelFactory val dispatcherProvider: DispatcherProvider @@ -494,6 +544,13 @@ interface WireMetroGraph { ): 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 @@ -572,6 +629,60 @@ interface WireMetroGraph { 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 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, @@ -655,6 +766,13 @@ interface WireMetroGraph { 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 @@ -949,6 +1067,66 @@ interface WireMetroGraph { 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 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 provideCheckConversationLeaveConditionsUseCase(conversationScope: ConversationScope): CheckConversationLeaveConditionsUseCase = + conversationScope.checkConversationLeaveConditions + + @Provides + fun provideClearConversationContentUseCase(conversationScope: ConversationScope): ClearConversationContentUseCase = + conversationScope.clearConversationContent + + @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 provideAppScope( @KaliumCoreLogic coreLogic: CoreLogic, @@ -1073,6 +1251,18 @@ interface WireMetroGraph { 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 @Suppress("LongParameterList") fun provideGetConversationsFromSearchUseCase( @@ -1119,6 +1309,11 @@ interface WireMetroGraph { 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 @@ -1161,6 +1356,20 @@ interface WireMetroGraph { dispatcher = dispatchers, ) + @Provides + fun provideExportObfuscatedCopyFileGateway( + fileManager: FileManager, + dispatchers: DispatcherProvider, + ): ExportObfuscatedCopyFileGateway = + AndroidExportObfuscatedCopyFileGateway( + fileManager = fileManager, + dispatcher = dispatchers, + ) + + @Provides + fun provideWorkManager(@ApplicationContext context: Context): WorkManager = + WorkManager.getInstance(context) + @Provides fun provideGetSessionsUseCase(@KaliumCoreLogic coreLogic: CoreLogic): GetSessionsUseCase = coreLogic.getGlobalScope().session.allSessions 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 7f0c69c344c..44fc2238573 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,7 +22,7 @@ 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.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.common.button.WireSecondaryIconButton import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.snackbar.collectAndShowSnackbar @@ -42,14 +42,11 @@ fun AddContactButton( userName: String, modifier: Modifier = Modifier, viewModel: ConnectionActionButtonViewModel = - wireViewModelScoped< - ConnectionActionButtonViewModelImpl, - ConnectionActionButtonViewModel, - ConnectionActionButtonArgs, - ConnectionActionButtonViewModelImpl.Factory - >( - ConnectionActionButtonArgs(userId, userName) - ), + ConnectionActionButtonArgs(userId, userName).let { args -> + metroViewModel(key = args.key) { + connectionActionButtonViewModelFactory.createImpl(args) + } + }, ) { val state = viewModel.actionableState() LocalSnackbarHostState.current.collectAndShowSnackbar(snackbarFlow = viewModel.infoMessage) 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 3a42b978ddb..8c556a4f63b 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.wireViewModelScoped +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.theme.WireTheme @@ -56,12 +56,13 @@ fun SecurityClassificationBannerForConversation( conversationId: ConversationId, modifier: Modifier = Modifier, viewModel: SecurityClassificationViewModel = - wireViewModelScoped< - SecurityClassificationViewModelImpl, - SecurityClassificationViewModel, - SecurityClassificationArgs, - SecurityClassificationViewModelImpl.Factory - >(SecurityClassificationArgs.Conversation(conversationId)) + SecurityClassificationArgs.Conversation(conversationId).let { args -> + metroViewModel( + key = args.key.toString(), + ) { + securityClassificationViewModelFactory.create(args) + } + } ) { SecurityClassificationBanner( state = viewModel.state(), @@ -74,14 +75,13 @@ fun SecurityClassificationBannerForUser( userId: UserId, modifier: Modifier = Modifier, viewModel: SecurityClassificationViewModel = - wireViewModelScoped< - SecurityClassificationViewModelImpl, - SecurityClassificationViewModel, - SecurityClassificationArgs, - SecurityClassificationViewModelImpl.Factory - >( - SecurityClassificationArgs.User(id = userId) - ) + SecurityClassificationArgs.User(id = userId).let { args -> + metroViewModel( + key = args.key.toString(), + ) { + securityClassificationViewModelFactory.create(args) + } + } ) { SecurityClassificationBanner( state = viewModel.state(), 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 index c727b4c379a..39009466148 100644 --- 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 @@ -25,8 +25,9 @@ import dev.zacsweers.metro.Inject class SecurityClassificationViewModelFactory( @KaliumCoreLogic private val coreLogic: CoreLogic, ) { - fun create(args: SecurityClassificationArgs): SecurityClassificationViewModel = SecurityClassificationViewModelImpl( - coreLogic = coreLogic, - args = args, - ) + 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 = - wireViewModelScoped() + metroViewModel { + conversationOptionsMenuViewModelFactory.create() + } ) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current @@ -189,6 +191,7 @@ private fun PreviewConversationOptionsModalSheetLayout(initialPage: Conversation onLeftConversation = {}, onDeletedConversation = {}, onDeletedConversationLocally = {}, + viewModel = object : ConversationOptionsMenuViewModel {}, ) } 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 index 81fa3666801..d853e4e2bdd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/connection/ConnectionActionButtonViewModelFactory.kt @@ -37,14 +37,17 @@ class ConnectionActionButtonViewModelFactory( private val unblockUser: UnblockUserUseCase, private val getOrCreateOneToOneConversation: GetOrCreateOneToOneConversationUseCase, ) { - fun create(args: ConnectionActionButtonArgs): ConnectionActionButtonViewModel = ConnectionActionButtonViewModelImpl( - dispatchers = dispatchers, - sendConnectionRequest = sendConnectionRequest, - cancelConnectionRequest = cancelConnectionRequest, - acceptConnectionRequest = acceptConnectionRequest, - ignoreConnectionRequest = ignoreConnectionRequest, - unblockUser = unblockUser, - getOrCreateOneToOneConversation = getOrCreateOneToOneConversation, - args = args, - ) + 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/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt index 1f8edbc0e5e..def0a7819c7 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.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl import com.wire.android.model.Clickable import com.wire.android.ui.common.WireDialog @@ -70,7 +70,9 @@ fun DebugDataOptions( onShowFeatureFlags: () -> Unit, onShowCryptoStats: () -> Unit, viewModel: DebugDataOptionsViewModel = - wireViewModelScoped() + metroViewModel { + debugDataOptionsViewModelFactory.create() + } ) { 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 84f6012b873..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 @@ -55,7 +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 kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -64,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") @@ -90,9 +88,7 @@ interface DebugDataOptionsViewModel { } @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class DebugDataOptionsViewModelImpl -@Inject constructor( +class DebugDataOptionsViewModelImpl( private val debugDataInfoProvider: DebugDataInfoProvider, @CurrentAccount val currentAccount: UserId, private val updateApiVersions: UpdateApiVersionsScheduler, 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 380e9d93500..7c826acd75d 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 @@ -44,7 +44,6 @@ import androidx.compose.ui.tooling.preview.Preview import com.wire.android.di.metro.metroViewModel import com.wire.android.BuildConfig import com.wire.android.R -import com.wire.android.di.wireViewModelScoped import com.wire.android.model.Clickable import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator @@ -151,7 +150,9 @@ internal fun UserDebugContent( fun DangerOptions( modifier: Modifier = Modifier, exportObfuscatedCopyViewModel: ExportObfuscatedCopyViewModel = - wireViewModelScoped() + metroViewModel { + exportObfuscatedCopyViewModelFactory.create() + } ) { Column(modifier = modifier) { 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 index 58492f742d9..0ca14fa97e1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyFileGateway.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyFileGateway.kt @@ -25,6 +25,7 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent +import dev.zacsweers.metro.Inject as MetroInject import kotlinx.coroutines.withContext import okio.Path import javax.inject.Inject @@ -34,7 +35,7 @@ interface ExportObfuscatedCopyFileGateway { suspend fun saveCopy(path: Path, destinationUri: String) } -class AndroidExportObfuscatedCopyFileGateway @Inject constructor( +class AndroidExportObfuscatedCopyFileGateway @Inject @MetroInject constructor( private val fileManager: FileManager, private val dispatcher: DispatcherProvider, ) : ExportObfuscatedCopyFileGateway { 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 57b512006b0..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 @@ -36,9 +36,7 @@ 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 javax.inject.Inject @ViewModelScopedPreview interface ExportObfuscatedCopyViewModel { @@ -54,8 +52,7 @@ interface ExportObfuscatedCopyViewModel { 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 fileGateway: ExportObfuscatedCopyFileGateway, 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/JoinConversationViaDeepLinkDialog.kt b/app/src/main/kotlin/com/wire/android/ui/joinConversation/JoinConversationViaDeepLinkDialog.kt index a2521a52263..988dbe4aa43 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 @@ -37,7 +37,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import com.wire.android.R -import com.wire.android.di.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.common.WireDialog import com.wire.android.ui.common.WireDialogButtonProperties import com.wire.android.ui.common.WireDialogButtonType @@ -79,7 +79,9 @@ fun JoinConversationViaDeepLinkDialog( onFlowCompleted: (conversationId: ConversationId?) -> Unit, modifier: Modifier = Modifier, ) { - val viewModel = wireViewModelScoped() + val viewModel: JoinConversationViaCodeViewModel = metroViewModel { + joinConversationViaCodeViewModelFactory.create() + } val isLoading: Boolean by remember { derivedStateOf { viewModel.state is JoinViaDeepLinkDialogState.Loading } From 2f08e38fc0e8de7725ff47c18292c73a1e85e785 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 06:08:32 +0200 Subject: [PATCH 19/46] refactor: migrate conversation action ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 137 ++++++++++++++++++ .../ui/connection/ConnectionActionButton.kt | 16 +- .../EditConversationMetadataViewModel.kt | 14 +- .../metadata/EditConversationNameScreen.kt | 6 +- .../AddMembersSearchScreen.kt | 9 +- .../AddMembersToConversationViewModel.kt | 14 +- .../search/apps/SearchAppsScreen.kt | 9 +- .../search/apps/SearchAppsViewModel.kt | 14 +- .../attachments/AdditionalOptionButton.kt | 18 ++- .../IsFileSharingEnabledViewModel.kt | 5 +- .../recordaudio/RecordAudioComponent.kt | 6 +- .../recordaudio/RecordAudioViewModel.kt | 5 +- 12 files changed, 180 insertions(+), 73 deletions(-) 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 index e8ac50a1c42..e09b7f9f2aa 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -18,6 +18,9 @@ package com.wire.android.di.metro import android.content.Context +import android.media.AudioAttributes +import android.media.AudioManager +import android.media.MediaPlayer import androidx.lifecycle.SavedStateHandle import androidx.work.WorkManager import com.wire.android.BuildConfig @@ -27,6 +30,7 @@ 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 @@ -36,7 +40,10 @@ import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.ObserveAppLockConfigUseCase import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl +import com.wire.android.media.audiomessage.AudioFocusHelper +import com.wire.android.media.audiomessage.RecordAudioMessagePlayer import com.wire.android.mapper.OtherAccountMapper +import com.wire.android.mapper.ContactMapper import com.wire.android.mapper.UserTypeMapper import com.wire.android.notification.WireNotificationManager import com.wire.android.ui.common.banner.SecurityClassificationViewModelFactory @@ -75,13 +82,22 @@ 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.metadata.EditConversationMetadataViewModelFactory import com.wire.android.ui.home.conversations.folder.NewFolderViewModelFactory 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.search.adddembertoconversation.AddMembersToConversationViewModelFactory +import com.wire.android.ui.home.conversations.search.apps.SearchAppsViewModelFactory import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase import com.wire.android.ui.home.gallery.MediaGalleryViewModelFactory +import com.wire.android.ui.home.messagecomposer.attachments.IsFileSharingEnabledViewModelFactory +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 @@ -138,8 +154,10 @@ 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.EMPTY import com.wire.android.util.FileManager +import com.wire.android.util.ScreenStateObserver import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.isWebsocketEnabledByDefault import com.wire.android.util.lifecycle.AutomatedLoginManager @@ -159,6 +177,7 @@ 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.asset.GetAvatarAssetUseCase +import com.wire.kalium.logic.feature.asset.AudioNormalizedLoudnessBuilder import com.wire.kalium.logic.feature.asset.GetAssetSizeLimitUseCase import com.wire.kalium.logic.feature.auth.AddAuthenticatedUserUseCase import com.wire.kalium.logic.feature.auth.LogoutUseCase @@ -169,7 +188,9 @@ 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 @@ -213,6 +234,7 @@ import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase +import com.wire.kalium.logic.feature.conversation.RenameConversationUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUseCase import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase @@ -254,7 +276,9 @@ import com.wire.kalium.logic.feature.team.TeamScope import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase 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.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase @@ -268,6 +292,7 @@ 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.IsPasswordRequiredUseCase import com.wire.kalium.logic.feature.user.IsReadOnlyAccountUseCase @@ -305,6 +330,8 @@ import dev.zacsweers.metro.DependencyGraph import dev.zacsweers.metro.Named import dev.zacsweers.metro.Provides import dev.zacsweers.metro.createGraphFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.runBlocking abstract class WireMetroScope private constructor() @@ -380,6 +407,11 @@ interface WireMetroGraph { val securityClassificationViewModelFactory: SecurityClassificationViewModelFactory val connectionActionButtonViewModelFactory: ConnectionActionButtonViewModelFactory val conversationOptionsMenuViewModelFactory: ConversationOptionsMenuViewModelFactory + val isFileSharingEnabledViewModelFactory: IsFileSharingEnabledViewModelFactory + val addMembersToConversationViewModelFactory: AddMembersToConversationViewModelFactory + val editConversationMetadataViewModelFactory: EditConversationMetadataViewModelFactory + val searchAppsViewModelFactory: SearchAppsViewModelFactory + val recordAudioViewModelFactory: RecordAudioViewModelFactory val dispatcherProvider: DispatcherProvider @@ -579,6 +611,80 @@ interface WireMetroGraph { ): AvatarImageManager = AvatarImageManager(context) + @Provides + fun provideAudioNormalizedLoudnessBuilder(@KaliumCoreLogic coreLogic: CoreLogic): AudioNormalizedLoudnessBuilder = + coreLogic.audioNormalizedLoudnessBuilder + + @ApplicationScope + @Provides + fun provideApplicationCoroutineScope(dispatchers: DispatcherProvider): CoroutineScope = + CoroutineScope(SupervisorJob() + dispatchers.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 + fun provideAudioFocusHelper(audioManager: AudioManager): AudioFocusHelper = + AudioFocusHelper(audioManager) + + @Provides + fun provideRecordAudioMessagePlayer( + @ApplicationContext context: Context, + mediaPlayer: MediaPlayer, + audioFocusHelper: AudioFocusHelper, + @ApplicationScope applicationScope: CoroutineScope, + ): RecordAudioMessagePlayer = + RecordAudioMessagePlayer( + context = context, + audioMediaPlayer = mediaPlayer, + audioFocusHelper = audioFocusHelper, + scope = applicationScope, + ) + + @Provides + fun provideScreenStateObserver(@ApplicationContext context: Context): ScreenStateObserver = + ScreenStateObserver(context) + + @Provides + fun provideCurrentScreenManager(screenStateObserver: ScreenStateObserver): CurrentScreenManager = + CurrentScreenManager(screenStateObserver) + + @Provides + fun provideGenerateAudioFileWithEffectsUseCase(dispatchers: DispatcherProvider): GenerateAudioFileWithEffectsUseCase = + GenerateAudioFileWithEffectsUseCase(dispatchers) + + @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, @@ -750,6 +856,10 @@ interface WireMetroGraph { fun provideUserTypeMapper(): UserTypeMapper = UserTypeMapper() + @Provides + fun provideContactMapper(userTypeMapper: UserTypeMapper): ContactMapper = + ContactMapper(userTypeMapper) + @Provides fun provideGetSelfUserUseCase(userScope: UserScope): GetSelfUserUseCase = userScope.getSelfUser @@ -837,6 +947,13 @@ interface WireMetroGraph { 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 @@ -1103,6 +1220,10 @@ interface WireMetroGraph { fun provideClearConversationContentUseCase(conversationScope: ConversationScope): ClearConversationContentUseCase = conversationScope.clearConversationContent + @Provides + fun provideRenameConversationUseCase(conversationScope: ConversationScope): RenameConversationUseCase = + conversationScope.renameConversation + @Provides fun provideSendConnectionRequestUseCase(connectionScope: ConnectionScope): SendConnectionRequestUseCase = connectionScope.sendConnectionRequest @@ -1142,6 +1263,14 @@ interface WireMetroGraph { 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, @@ -1157,6 +1286,14 @@ interface WireMetroGraph { 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 provideObserveIsAppsAllowedForUsageUseCase(serviceScope: ServiceScope): ObserveIsAppsAllowedForUsageUseCase = serviceScope.observeIsAppsAllowedForUsage 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 ec4262f7d6d..7685140fb75 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.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel 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,14 +74,11 @@ fun ConnectionActionButton( onConnectionRequestIgnored: (String) -> Unit = {}, onOpenConversation: (ConversationId) -> Unit = {}, viewModel: ConnectionActionButtonViewModel = - wireViewModelScoped< - ConnectionActionButtonViewModelImpl, - ConnectionActionButtonViewModel, - ConnectionActionButtonArgs, - ConnectionActionButtonViewModelImpl.Factory - >( - ConnectionActionButtonArgs(userId, userName) - ), + ConnectionActionButtonArgs(userId, userName).let { args -> + metroViewModel(key = args.key) { + connectionActionButtonViewModelFactory.createImpl(args) + } + }, ) { LocalSnackbarHostState.current.collectAndShowSnackbar(snackbarFlow = viewModel.infoMessage) val unblockUserDialogState = rememberVisibilityState() 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 84ecd4edc8d..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 @@ -35,10 +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.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.dropWhile import kotlinx.coroutines.flow.filterIsInstance @@ -47,9 +43,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -@HiltViewModel(assistedFactory = EditConversationMetadataViewModel.Factory::class) -class EditConversationMetadataViewModel @AssistedInject constructor( - @Assisted private val editConversationNameNavArgs: EditConversationNameNavArgs, +class EditConversationMetadataViewModel( + private val editConversationNameNavArgs: EditConversationNameNavArgs, private val dispatcher: DispatcherProvider, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val renameConversation: RenameConversationUseCase, @@ -66,11 +61,6 @@ class EditConversationMetadataViewModel @AssistedInject constructor( observeConversationNameChanges() } - @AssistedFactory - interface Factory { - fun create(args: EditConversationNameNavArgs): EditConversationMetadataViewModel - } - private fun getConversationDetails() { viewModelScope.launch { val conversationDetails = observeConversationDetails(conversationId) 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 a3feffc73a0..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 @@ -42,9 +42,7 @@ fun EditConversationNameScreen( resultNavigator: ResultBackNavigator, args: EditConversationNameNavArgs, viewModel: EditConversationMetadataViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { editConversationMetadataViewModelFactory.create(args) }, ) { with(viewModel) { LaunchedEffect(editConversationState.completed) { 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 6ae3b5b0fd6..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,10 +42,9 @@ import com.wire.kalium.logic.data.user.UserId fun AddMembersSearchScreen( navigator: Navigator, navArgs: AddMembersSearchNavArgs, - addMembersToConversationViewModel: AddMembersToConversationViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(navArgs) } - ), + addMembersToConversationViewModel: AddMembersToConversationViewModel = metroViewModel { + addMembersToConversationViewModelFactory.create(navArgs) + }, ) { if (addMembersToConversationViewModel.newGroupState.isCompleted) { navigator.navigateBack() 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 5ffe17290f2..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 @@ -28,19 +28,14 @@ import com.wire.android.ui.home.newconversation.model.Contact 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -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 -@HiltViewModel(assistedFactory = AddMembersToConversationViewModel.Factory::class) -class AddMembersToConversationViewModel @AssistedInject constructor( - @Assisted private val addMembersSearchNavArgs: AddMembersSearchNavArgs, +class AddMembersToConversationViewModel( + private val addMembersSearchNavArgs: AddMembersSearchNavArgs, private val addMemberToConversation: AddMemberToConversationUseCase, private val dispatchers: DispatcherProvider ) : ViewModel() { @@ -73,11 +68,6 @@ class AddMembersToConversationViewModel @AssistedInject constructor( ) } } - - @AssistedFactory - interface Factory { - fun create(args: AddMembersSearchNavArgs): AddMembersToConversationViewModel - } } data class AddMembersToConversationState( 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/messagecomposer/attachments/AdditionalOptionButton.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/attachments/AdditionalOptionButton.kt index 4fec3922c93..93c9ee4391e 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.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel 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,9 @@ fun AdditionalOptionButton( onClick: () -> Unit, modifier: Modifier = Modifier, viewModel: IsFileSharingEnabledViewModel = - wireViewModelScoped() + metroViewModel { + isFileSharingEnabledViewModelFactory.create() + } ) { var enableAgain by remember { mutableStateOf(true) } LaunchedEffect(enableAgain, block = { @@ -84,7 +86,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 +98,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/recordaudio/RecordAudioComponent.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioComponent.kt index f28440e3eca..89c4db7e2d9 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.wire.android.di.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel import com.wire.android.ui.common.HandleActions import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions @@ -50,9 +50,9 @@ fun RecordAudioComponent( onAudioRecorded: (UriAsset) -> Unit, onCloseRecordAudio: () -> Unit, modifier: Modifier = Modifier, - lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + viewModel: RecordAudioViewModel = metroViewModel { recordAudioViewModelFactory.create() } ) { - val viewModel: RecordAudioViewModel = wireViewModelScoped() val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current 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 3bbcdcaabf2..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 @@ -40,7 +40,6 @@ 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 kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -48,12 +47,10 @@ 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( +class RecordAudioViewModel( private val recordAudioMessagePlayer: RecordAudioMessagePlayer, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val getAssetSizeLimit: GetAssetSizeLimitUseCase, From 4b5cbc02713ea2b6ffd3b5b7dc6adeb9272b3964 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 06:21:53 +0200 Subject: [PATCH 20/46] refactor: migrate conversation settings ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 112 ++++++++++++++++++ .../editguestaccess/EditGuestAccessScreen.kt | 6 +- .../EditGuestAccessViewModel.kt | 14 +-- .../CreatePasswordGuestLinkViewModel.kt | 14 +-- .../CreatePasswordProtectedGuestLinkScreen.kt | 6 +- .../EditSelfDeletingMessagesScreen.kt | 6 +- .../EditSelfDeletingMessagesViewModel.kt | 14 +-- .../UpdateAppsAccessScreen.kt | 6 +- .../UpdateAppsAccessViewModel.kt | 14 +-- .../ChannelAccessOnUpdateScreen.kt | 6 +- .../UpdateChannelAccessViewModel.kt | 14 +-- .../SearchConversationMessagesScreen.kt | 9 +- .../SearchConversationMessagesViewModel.kt | 16 +-- 13 files changed, 139 insertions(+), 98 deletions(-) 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 index e09b7f9f2aa..95c2839899c 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -44,6 +44,7 @@ import com.wire.android.media.audiomessage.AudioFocusHelper import com.wire.android.media.audiomessage.RecordAudioMessagePlayer import com.wire.android.mapper.OtherAccountMapper import com.wire.android.mapper.ContactMapper +import com.wire.android.mapper.UIParticipantMapper import com.wire.android.mapper.UserTypeMapper import com.wire.android.notification.WireNotificationManager import com.wire.android.ui.common.banner.SecurityClassificationViewModelFactory @@ -83,12 +84,20 @@ import com.wire.android.ui.home.appLock.unlock.AppUnlockWithBiometricsViewModelF 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.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.folder.NewFolderViewModelFactory +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.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.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.usecase.GetConversationMessagesFromSearchUseCase import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase import com.wire.android.ui.home.gallery.MediaGalleryViewModelFactory @@ -201,6 +210,7 @@ import com.wire.kalium.logic.feature.backup.VerifyBackupUseCase import com.wire.kalium.logic.feature.call.CallsScope import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.channels.ChannelsScope import com.wire.kalium.logic.feature.client.ClientFingerprintUseCase import com.wire.kalium.logic.feature.client.ClientScope import com.wire.kalium.logic.feature.client.DeleteClientUseCase @@ -235,10 +245,15 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseC import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase import com.wire.kalium.logic.feature.conversation.RenameConversationUseCase +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.apps.ChangeAccessForAppsInConversationUseCase +import com.wire.kalium.logic.feature.conversation.channel.UpdateChannelAddPermissionUseCase 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 @@ -247,6 +262,10 @@ import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFro 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 @@ -262,6 +281,7 @@ import com.wire.kalium.logic.feature.debug.SetDebugE2EICertificateExpirationUseC import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsUseCase import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase 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 @@ -317,9 +337,11 @@ import com.wire.kalium.logic.feature.user.typingIndicator.ObserveTypingIndicator 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.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.util.DelicateKaliumApi import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.EntryPoint @@ -412,6 +434,12 @@ interface WireMetroGraph { 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 dispatcherProvider: DispatcherProvider @@ -860,10 +888,18 @@ interface WireMetroGraph { fun provideContactMapper(userTypeMapper: UserTypeMapper): ContactMapper = ContactMapper(userTypeMapper) + @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 provideGetMLSClientIdentityUseCase(userScope: UserScope): GetMLSClientIdentityUseCase = userScope.getE2EICertificate @@ -1192,6 +1228,20 @@ interface WireMetroGraph { 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 @@ -1224,6 +1274,60 @@ interface WireMetroGraph { 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 provideSendConnectionRequestUseCase(connectionScope: ConnectionScope): SendConnectionRequestUseCase = connectionScope.sendConnectionRequest @@ -1421,6 +1525,12 @@ interface WireMetroGraph { uiTextResolver = uiTextResolver, ) + @Provides + fun provideGetConversationMessagesFromSearchUseCase( + entryPoint: WireMetroHiltEntryPoint, + ): GetConversationMessagesFromSearchUseCase = + entryPoint.getConversationMessagesFromSearchUseCase() + @Provides fun provideObserveSelfDeletionTimerSettingsForConversationUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @@ -1604,4 +1714,6 @@ interface WireMetroHiltEntryPoint { fun logFileWriter(): LogFileWriter fun accountSwitchUseCase(): AccountSwitchUseCase + + fun getConversationMessagesFromSearchUseCase(): GetConversationMessagesFromSearchUseCase } 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 3f9ef68338b..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 @@ -73,9 +73,7 @@ fun EditGuestAccessScreen( args: EditGuestAccessNavArgs, modifier: Modifier = Modifier, editGuestAccessViewModel: EditGuestAccessViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + metroViewModel { editGuestAccessViewModelFactory.create(args) } ) { val scrollState = rememberScrollState() val snackbarHostState = LocalSnackbarHostState.current 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 d5c7f0ec5f0..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 @@ -45,10 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -61,10 +57,9 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -@HiltViewModel(assistedFactory = EditGuestAccessViewModel.Factory::class) @Suppress("LongParameterList", "TooManyFunctions") -class EditGuestAccessViewModel @AssistedInject constructor( - @Assisted private val editGuestAccessNavArgs: EditGuestAccessNavArgs, +class EditGuestAccessViewModel( + private val editGuestAccessNavArgs: EditGuestAccessNavArgs, private val dispatcher: DispatcherProvider, private val updateConversationAccessRole: UpdateConversationAccessRoleUseCase, private val observeConversationDetails: ObserveConversationDetailsUseCase, @@ -97,11 +92,6 @@ class EditGuestAccessViewModel @AssistedInject constructor( checkIfUserCanCreatePasswordProtectedLinks() } - @AssistedFactory - interface Factory { - fun create(args: EditGuestAccessNavArgs): EditGuestAccessViewModel - } - private val syncCodeMutex = Mutex() private var isGuestCodeUpdated = false 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 5597b590af7..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 @@ -30,18 +30,13 @@ 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.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.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = CreatePasswordGuestLinkViewModel.Factory::class) -class CreatePasswordGuestLinkViewModel @AssistedInject constructor( - @Assisted private val createPasswordGuestLinkNavArgs: CreatePasswordGuestLinkNavArgs, +class CreatePasswordGuestLinkViewModel( + createPasswordGuestLinkNavArgs: CreatePasswordGuestLinkNavArgs, private val generateGuestRoomLink: GenerateGuestRoomLinkUseCase, private val validatePassword: ValidatePasswordUseCase, private val generatePassword: RandomPassword, @@ -52,11 +47,6 @@ class CreatePasswordGuestLinkViewModel @AssistedInject constructor( var state by mutableStateOf(CreatePasswordGuestLinkState()) @VisibleForTesting set - @AssistedFactory - interface Factory { - fun create(args: CreatePasswordGuestLinkNavArgs): CreatePasswordGuestLinkViewModel - } - suspend fun observePasswordValidation() { combine( state.passwordTextState.textAsFlow(), 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 bd546df005a..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 @@ -75,9 +75,7 @@ fun CreatePasswordProtectedGuestLinkScreen( navigator: Navigator, args: CreatePasswordGuestLinkNavArgs, viewModel: CreatePasswordGuestLinkViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + 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 b73ad014048..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 @@ -70,9 +70,7 @@ fun EditSelfDeletingMessagesScreen( navigator: Navigator, args: EditSelfDeletingMessagesNavArgs, editSelfDeletingMessagesViewModel: EditSelfDeletingMessagesViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { editSelfDeletingMessagesViewModelFactory.create(args) }, ) { val scrollState = rememberScrollState() val context = LocalContext.current 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 abcd0d02ae0..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 @@ -35,20 +35,15 @@ 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -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 -@HiltViewModel(assistedFactory = EditSelfDeletingMessagesViewModel.Factory::class) @Suppress("LongParameterList", "TooManyFunctions") -class EditSelfDeletingMessagesViewModel @AssistedInject constructor( - @Assisted private val editSelfDeletingMessagesNavArgs: EditSelfDeletingMessagesNavArgs, +class EditSelfDeletingMessagesViewModel( + private val editSelfDeletingMessagesNavArgs: EditSelfDeletingMessagesNavArgs, private val dispatcher: DispatcherProvider, private val observeConversationMembers: ObserveParticipantsForConversationUseCase, private val observeSelfDeletionTimerSettingsForConversation: ObserveSelfDeletionTimerSettingsForConversationUseCase, @@ -67,11 +62,6 @@ class EditSelfDeletingMessagesViewModel @AssistedInject constructor( observeSelfDeletionTimerSettingsForConversation() } - @AssistedFactory - interface Factory { - fun create(args: EditSelfDeletingMessagesNavArgs): EditSelfDeletingMessagesViewModel - } - private fun observeSelfDeletionTimerSettingsForConversation() { viewModelScope.launch { // TODO(refactor): Move all this logic to a UseCase 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 58521674832..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 @@ -56,9 +56,7 @@ fun UpdateAppsAccessScreen( navigator: Navigator, args: UpdateAppsAccessNavArgs, updateAppsAccessViewModel: UpdateAppsAccessViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + 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 3dabcd61058..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 @@ -37,10 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -51,9 +47,8 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -@HiltViewModel(assistedFactory = UpdateAppsAccessViewModel.Factory::class) -class UpdateAppsAccessViewModel @AssistedInject constructor( - @Assisted private val updateAppsAccessNavArgs: UpdateAppsAccessNavArgs, +class UpdateAppsAccessViewModel( + private val updateAppsAccessNavArgs: UpdateAppsAccessNavArgs, private val dispatcher: DispatcherProvider, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val observeConversationMembers: ObserveParticipantsForConversationUseCase, @@ -79,11 +74,6 @@ class UpdateAppsAccessViewModel @AssistedInject constructor( observeConversationDetails() } - @AssistedFactory - interface Factory { - fun create(args: UpdateAppsAccessNavArgs): UpdateAppsAccessViewModel - } - @Suppress("DestructuringDeclarationWithTooManyEntries") private fun observeConversationDetails() { viewModelScope.launch { 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 8492fb26df3..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 @@ -40,9 +40,7 @@ fun ChannelAccessOnUpdateScreen( resultNavigator: ResultBackNavigator, args: UpdateChannelAccessArgs, updateChannelAccessViewModel: UpdateChannelAccessViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + 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 e7ab6678839..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 @@ -29,15 +29,10 @@ 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = UpdateChannelAccessViewModel.Factory::class) -class UpdateChannelAccessViewModel @AssistedInject constructor( - @Assisted private val channelAccessNavArgs: UpdateChannelAccessArgs, +class UpdateChannelAccessViewModel( + private val channelAccessNavArgs: UpdateChannelAccessArgs, val updateChannelAddPermission: UpdateChannelAddPermissionUseCase, private val qualifiedIdMapper: QualifiedIdMapper, ) : ViewModel() { @@ -50,11 +45,6 @@ class UpdateChannelAccessViewModel @AssistedInject constructor( val conversationId: String = channelAccessNavArgs.conversationId - @AssistedFactory - interface Factory { - fun create(args: UpdateChannelAccessArgs): UpdateChannelAccessViewModel - } - fun updateChannelAddPermission(newPermission: ChannelAddPermissionType) { viewModelScope.launch { val result = updateChannelAddPermission( 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 82129a8eba8..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 @@ -60,10 +60,9 @@ import com.wire.android.ui.common.R as commonR fun SearchConversationMessagesScreen( navigator: Navigator, navArgs: SearchConversationMessagesNavArgs, - searchConversationMessagesViewModel: SearchConversationMessagesViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(navArgs) } - ) + 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 e9c6beb8ef4..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 @@ -22,24 +22,19 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue 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.wire.android.util.dispatchers.DispatcherProvider 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.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach -@HiltViewModel(assistedFactory = SearchConversationMessagesViewModel.Factory::class) -class SearchConversationMessagesViewModel @AssistedInject constructor( - @Assisted searchConversationMessagesNavArgs: SearchConversationMessagesNavArgs, +class SearchConversationMessagesViewModel( + searchConversationMessagesNavArgs: SearchConversationMessagesNavArgs, private val getSearchMessagesForConversation: GetConversationMessagesFromSearchUseCase, private val dispatchers: DispatcherProvider ) : ViewModel() { @@ -77,9 +72,4 @@ class SearchConversationMessagesViewModel @AssistedInject constructor( searchResult = messagesResultFlow, ) } - - @AssistedFactory - interface Factory { - fun create(args: SearchConversationMessagesNavArgs): SearchConversationMessagesViewModel - } } From 985751d1ba58970b9e34b64f3ee99c8c53fffb36 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 06:26:52 +0200 Subject: [PATCH 21/46] refactor: migrate conversation utility ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 111 ++++++++++++++++++ .../conversations/UsersTypingIndicator.kt | 17 ++- .../GroupConversationAllParticipantsScreen.kt | 6 +- .../GroupConversationParticipantsViewModel.kt | 14 +-- .../folder/ConversationFoldersScreen.kt | 16 ++- .../folder/ConversationFoldersVM.kt | 14 +-- .../folder/MoveConversationToFolderVM.kt | 14 +-- .../messagedetails/MessageDetailsScreen.kt | 8 +- .../messagedetails/MessageDetailsViewModel.kt | 14 +-- .../search/SearchUserViewModel.kt | 14 +-- .../search/SearchUsersAndAppsScreen.kt | 10 +- .../typing/TypingIndicatorViewModel.kt | 15 +-- .../all/AllConversationsScreen.kt | 7 +- .../location/LocationPickerComponent.kt | 4 +- .../location/LocationPickerViewModel.kt | 5 +- 15 files changed, 154 insertions(+), 115 deletions(-) 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 index 95c2839899c..9d8e15503d0 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -83,25 +83,36 @@ 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.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.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.messagedetails.MessageDetailsViewModelFactory +import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveReactionsForMessageUseCase +import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveReceiptsForMessageUseCase 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.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.usecase.GetConversationMessagesFromSearchUseCase import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase +import com.wire.android.ui.home.conversations.usecase.ObserveUsersTypingInConversationUseCase +import com.wire.android.ui.home.conversations.typing.TypingIndicatorViewModelFactory import com.wire.android.ui.home.gallery.MediaGalleryViewModelFactory import com.wire.android.ui.home.messagecomposer.attachments.IsFileSharingEnabledViewModelFactory +import com.wire.android.ui.home.messagecomposer.location.LocationPickerHelperFlavor +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 @@ -225,6 +236,7 @@ import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase 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 @@ -258,6 +270,7 @@ import com.wire.kalium.logic.feature.conversation.getPaginatedFlowOfConversation 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 @@ -305,6 +318,14 @@ import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletion import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import com.wire.kalium.logic.feature.message.DeleteMessageUseCase import com.wire.kalium.logic.feature.message.MessageScope +import com.wire.kalium.logic.feature.message.ObserveMessageReactionsUseCase +import com.wire.kalium.logic.feature.message.ObserveMessageReceiptsUseCase +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.team.SyncSelfTeamInfoUseCase import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCase import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase @@ -440,6 +461,13 @@ interface WireMetroGraph { 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 dispatcherProvider: DispatcherProvider @@ -550,6 +578,10 @@ interface WireMetroGraph { fun provideWireNotificationManager(entryPoint: WireMetroHiltEntryPoint): WireNotificationManager = entryPoint.wireNotificationManager() + @Provides + fun provideLocationPickerHelperFlavor(entryPoint: WireMetroHiltEntryPoint): LocationPickerHelperFlavor = + entryPoint.locationPickerHelperFlavor() + @Provides fun provideLogFileWriter(entryPoint: WireMetroHiltEntryPoint): LogFileWriter = entryPoint.logFileWriter() @@ -900,6 +932,10 @@ interface WireMetroGraph { fun provideGetMembersE2EICertificateStatusesUseCase(userScope: UserScope): GetMembersE2EICertificateStatusesUseCase = userScope.getMembersE2EICertificateStatuses + @Provides + fun provideRefreshUsersWithoutMetadataUseCase(userScope: UserScope): RefreshUsersWithoutMetadataUseCase = + userScope.refreshUsersWithoutMetadata + @Provides fun provideGetMLSClientIdentityUseCase(userScope: UserScope): GetMLSClientIdentityUseCase = userScope.getE2EICertificate @@ -1184,10 +1220,28 @@ interface WireMetroGraph { 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 provideObserveUsersTypingUseCase(conversationScope: ConversationScope): ObserveUsersTypingUseCase = + conversationScope.observeUsersTyping + + @Provides + fun provideObserveUsersTypingInConversationUseCase( + observeUsersTyping: ObserveUsersTypingUseCase, + uiParticipantMapper: UIParticipantMapper, + ): ObserveUsersTypingInConversationUseCase = + ObserveUsersTypingInConversationUseCase( + observeUsersTyping = observeUsersTyping, + uiParticipantMapper = uiParticipantMapper, + ) + @Provides fun provideObserveConversationRoleForUserUseCase( observeConversationMembers: ObserveConversationMembersUseCase, @@ -1352,6 +1406,29 @@ interface WireMetroGraph { 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, @@ -1417,6 +1494,38 @@ interface WireMetroGraph { fun provideDeleteMessageUseCase(messageScope: MessageScope): DeleteMessageUseCase = messageScope.deleteMessage + @Provides + fun provideObserveMessageReactionsUseCase(messageScope: MessageScope): ObserveMessageReactionsUseCase = + messageScope.observeMessageReactions + + @Provides + fun provideObserveMessageReceiptsUseCase(messageScope: MessageScope): ObserveMessageReceiptsUseCase = + messageScope.observeMessageReceipts + + @Provides + fun provideObserveReactionsForMessageUseCase( + observeMessageReactions: ObserveMessageReactionsUseCase, + uiParticipantMapper: UIParticipantMapper, + dispatchers: DispatcherProvider, + ): ObserveReactionsForMessageUseCase = + ObserveReactionsForMessageUseCase( + observeMessageReactions = observeMessageReactions, + uiParticipantMapper = uiParticipantMapper, + dispatchers = dispatchers, + ) + + @Provides + fun provideObserveReceiptsForMessageUseCase( + observeMessageReceipts: ObserveMessageReceiptsUseCase, + uiParticipantMapper: UIParticipantMapper, + dispatchers: DispatcherProvider, + ): ObserveReceiptsForMessageUseCase = + ObserveReceiptsForMessageUseCase( + observeMessageReceipts = observeMessageReceipts, + uiParticipantMapper = uiParticipantMapper, + dispatchers = dispatchers, + ) + @Provides fun provideCellsScope( @KaliumCoreLogic coreLogic: CoreLogic, @@ -1711,6 +1820,8 @@ interface WireMetroHiltEntryPoint { fun wireNotificationManager(): WireNotificationManager + fun locationPickerHelperFlavor(): LocationPickerHelperFlavor + fun logFileWriter(): LogFileWriter fun accountSwitchUseCase(): AccountSwitchUseCase 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 ca61d3c939c..aed92995938 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.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.NameBasedAvatar import com.wire.android.model.UserAvatarData import com.wire.android.ui.common.avatar.UserProfileAvatarsRow @@ -74,14 +74,13 @@ private const val ANIMATION_SPEED_MILLIS = 1_500 fun UsersTypingIndicatorForConversation( conversationId: ConversationId, viewModel: TypingIndicatorViewModel = - wireViewModelScoped< - TypingIndicatorViewModelImpl, - TypingIndicatorViewModel, - TypingIndicatorArgs, - TypingIndicatorViewModelImpl.Factory - >( - TypingIndicatorArgs(conversationId) - ) + TypingIndicatorArgs(conversationId).let { args -> + metroViewModel( + key = args.key.toString(), + ) { + typingIndicatorViewModelFactory.create(args) + } + } ) { UsersTypingIndicator(usersTyping = viewModel.state().usersTyping) } 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 10954eff14d..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 @@ -58,9 +58,7 @@ fun GroupConversationAllParticipantsScreen( navigator: Navigator, navArgs: GroupConversationAllParticipantsNavArgs, viewModel: GroupConversationParticipantsViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(navArgs.conversationId) } - ) + 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 eadc5b39ab6..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 @@ -26,15 +26,10 @@ import androidx.lifecycle.viewModelScope 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 dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = GroupConversationParticipantsViewModel.Factory::class) -open class GroupConversationParticipantsViewModel @AssistedInject constructor( - @Assisted private val conversationId: QualifiedID, +open class GroupConversationParticipantsViewModel( + private val conversationId: QualifiedID, private val observeConversationMembers: ObserveParticipantsForConversationUseCase, private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, ) : ViewModel() { @@ -48,11 +43,6 @@ open class GroupConversationParticipantsViewModel @AssistedInject constructor( observeConversationMembers() } - @AssistedFactory - interface Factory { - fun create(conversationId: QualifiedID): GroupConversationParticipantsViewModel - } - private fun runRefreshUsersWithoutMetadata() { viewModelScope.launch { refreshUsersWithoutMetadata() 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/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/messagedetails/MessageDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messagedetails/MessageDetailsScreen.kt index d8bd7587a9c..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 @@ -68,9 +68,7 @@ import kotlinx.coroutines.launch fun MessageDetailsScreen( navigator: Navigator, args: MessageDetailsNavArgs, - viewModel: MessageDetailsViewModel = hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ) + 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 3ec14ffb2cb..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 @@ -27,15 +27,10 @@ import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveReac import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveReceiptsForMessageUseCase import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.message.receipt.ReceiptType -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = MessageDetailsViewModel.Factory::class) -class MessageDetailsViewModel @AssistedInject constructor( - @Assisted private val messageDetailsNavArgs: MessageDetailsNavArgs, +class MessageDetailsViewModel( + private val messageDetailsNavArgs: MessageDetailsNavArgs, private val observeReactionsForMessage: ObserveReactionsForMessageUseCase, private val observeReceiptsForMessage: ObserveReceiptsForMessageUseCase ) : ViewModel() { @@ -46,11 +41,6 @@ class MessageDetailsViewModel @AssistedInject constructor( var messageDetailsState: MessageDetailsState by mutableStateOf(MessageDetailsState()) - @AssistedFactory - interface Factory { - fun create(args: MessageDetailsNavArgs): MessageDetailsViewModel - } - init { viewModelScope.launch { messageDetailsState = messageDetailsState.copy( 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 05eecfc8e89..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 @@ -33,10 +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.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.ImmutableSet import kotlinx.collections.immutable.persistentListOf @@ -50,9 +46,8 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = SearchUserViewModel.Factory::class) -class SearchUserViewModel @AssistedInject constructor( - @Assisted private val addMembersSearchNavArgs: AddMembersSearchNavArgs?, +class SearchUserViewModel( + private val addMembersSearchNavArgs: AddMembersSearchNavArgs?, private val searchUserUseCase: SearchUsersUseCase, private val searchByHandleUseCase: SearchByHandleUseCase, private val contactMapper: ContactMapper, @@ -154,11 +149,6 @@ class SearchUserViewModel @AssistedInject constructor( excludingMembersOfConversation = addMembersSearchNavArgs?.conversationId, customDomain = domain ) - - @AssistedFactory - interface Factory { - fun create(addMembersSearchNavArgs: AddMembersSearchNavArgs?): SearchUserViewModel - } } data class SearchUserState( 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 6e038471cdb..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 @@ -264,9 +264,11 @@ private fun SearchAllPeopleOrContactsScreen( onOpenUserProfile: (Contact) -> Unit, onContactChecked: (Boolean, Contact) -> Unit, addMembersSearchNavArgs: AddMembersSearchNavArgs? = null, - searchUserViewModel: SearchUserViewModel = hiltViewModel( - creationCallback = { factory -> factory.create(addMembersSearchNavArgs) } - ), + 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/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/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/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/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 From 1b6004ceff5b94f3afee4b2f31b73094732bc524 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 06:44:13 +0200 Subject: [PATCH 22/46] refactor: migrate message component ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 341 ++++++++++++++++++ .../audiomessage/AudioMessageViewModel.kt | 15 +- .../CompositeMessageViewModel.kt | 15 +- .../home/conversations/ConversationScreen.kt | 5 +- .../details/GroupConversationDetailsScreen.kt | 13 +- .../GroupConversationDetailsViewModel.kt | 16 +- .../options/GroupConversationOptions.kt | 11 +- .../edit/MessageOptionsMenuViewModel.kt | 15 +- .../edit/MessageOptionsModalSheetLayout.kt | 13 +- .../ConversationAssetMessagesViewModel.kt | 14 +- .../media/ConversationMediaScreen.kt | 16 +- .../messages/ConversationMessagesViewModel.kt | 14 +- .../conversations/messages/QuotedMessage.kt | 29 +- .../messages/QuotedMultipartMessage.kt | 6 +- .../QuotedMultipartMessageViewModel.kt | 5 +- .../messages/item/AssetLocalPathViewModel.kt | 15 +- .../item/AssetLocalPathViewModelFactory.kt | 2 +- .../item/ConversationAssetPathsViewModel.kt | 5 +- .../messages/item/MessageContentAndStatus.kt | 10 +- .../home/conversations/model/MessageTypes.kt | 15 +- .../messagetypes/audio/AudioMessageType.kt | 26 +- .../multipart/MultipartAttachmentsView.kt | 6 +- .../MultipartAttachmentsViewModel.kt | 5 +- .../messagecomposer/MessageComposeActions.kt | 17 +- .../messagecomposer/MessageComposerInput.kt | 15 +- .../SelfDeletingMessageActionViewModel.kt | 15 +- 26 files changed, 442 insertions(+), 217 deletions(-) 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 index 9d8e15503d0..ede80e67b56 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -21,6 +21,7 @@ import android.content.Context import android.media.AudioAttributes import android.media.AudioManager import android.media.MediaPlayer +import android.os.Build import androidx.lifecycle.SavedStateHandle import androidx.work.WorkManager import com.wire.android.BuildConfig @@ -40,10 +41,19 @@ import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.ObserveAppLockConfigUseCase import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.feature.analytics.AnonymousAnalyticsManagerImpl +import com.wire.android.feature.cells.ui.edit.OnlineEditor import com.wire.android.media.audiomessage.AudioFocusHelper +import com.wire.android.media.audiomessage.AudioMessageViewModelFactory +import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer import com.wire.android.media.audiomessage.RecordAudioMessagePlayer +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.UIParticipantMapper import com.wire.android.mapper.UserTypeMapper import com.wire.android.notification.WireNotificationManager @@ -88,29 +98,48 @@ import com.wire.android.ui.home.conversations.details.metadata.EditConversationM 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.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.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.messagedetails.usecase.ObserveReactionsForMessageUseCase import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveReceiptsForMessageUseCase 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.model.messagetypes.multipart.CellAssetRefreshHelper +import com.wire.android.ui.home.conversations.model.messagetypes.multipart.MultipartAttachmentsViewModelFactory 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.usecase.GetAssetMessagesFromConversationUseCase import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase +import com.wire.android.ui.home.conversations.usecase.GetMessagesForConversationUseCase +import com.wire.android.ui.home.conversations.usecase.GetUsersForMessageUseCase import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase +import com.wire.android.ui.home.conversations.usecase.ObserveImageAssetMessagesFromConversationUseCase +import com.wire.android.ui.home.conversations.usecase.ObserveMessageForConversationUseCase +import com.wire.android.ui.home.conversations.usecase.ObserveQuoteMessageForConversationUseCase import com.wire.android.ui.home.conversations.usecase.ObserveUsersTypingInConversationUseCase import com.wire.android.ui.home.conversations.typing.TypingIndicatorViewModelFactory import com.wire.android.ui.home.gallery.MediaGalleryViewModelFactory 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.LocationPickerHelperFlavor import com.wire.android.ui.home.messagecomposer.location.LocationPickerViewModelFactory import com.wire.android.ui.home.messagecomposer.recordaudio.AndroidRecordAudioFileGateway @@ -182,12 +211,18 @@ 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.logging.LogFileWriter +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.kalium.cells.CellsScope +import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetMessageAttachmentUseCase +import com.wire.kalium.cells.domain.usecase.GetWireCellConfigurationUseCase +import com.wire.kalium.cells.domain.usecase.RefreshCellAssetStateUseCase +import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.data.conversation.FetchConversationUseCase @@ -199,6 +234,10 @@ import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseC import com.wire.kalium.logic.feature.asset.GetAvatarAssetUseCase import com.wire.kalium.logic.feature.asset.AudioNormalizedLoudnessBuilder 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.auth.AddAuthenticatedUserUseCase import com.wire.kalium.logic.feature.auth.LogoutUseCase import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase @@ -230,6 +269,7 @@ 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.IsWireCellsEnabledUseCase import com.wire.kalium.logic.feature.client.ObserveClientDetailsUseCase import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase @@ -246,8 +286,10 @@ 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 @@ -255,6 +297,7 @@ import com.wire.kalium.logic.feature.conversation.JoinConversationViaCodeUseCase import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase +import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase import com.wire.kalium.logic.feature.conversation.RenameConversationUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationCodeUseCase @@ -262,6 +305,7 @@ import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUs 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.delete.MarkConversationAsDeletedLocallyUseCase @@ -317,15 +361,27 @@ import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTim 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.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.ToggleReactionUseCase +import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase +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.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 @@ -335,6 +391,7 @@ 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 @@ -359,6 +416,8 @@ import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicator 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.ObserveSyncStateUseCase import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase @@ -468,6 +527,17 @@ interface WireMetroGraph { 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 dispatcherProvider: DispatcherProvider @@ -594,6 +664,39 @@ interface WireMetroGraph { fun provideAnonymousAnalyticsManager(): AnonymousAnalyticsManager = AnonymousAnalyticsManagerImpl + @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() @@ -713,6 +816,10 @@ interface WireMetroGraph { scope = applicationScope, ) + @Provides + fun provideConversationAudioMessagePlayer(entryPoint: WireMetroHiltEntryPoint): ConversationAudioMessagePlayer = + entryPoint.conversationAudioMessagePlayer() + @Provides fun provideScreenStateObserver(@ApplicationContext context: Context): ScreenStateObserver = ScreenStateObserver(context) @@ -920,6 +1027,44 @@ interface WireMetroGraph { fun provideContactMapper(userTypeMapper: UserTypeMapper): ContactMapper = ContactMapper(userTypeMapper) + @Provides + fun provideMessageResourceProvider(): MessageResourceProvider = + MessageResourceProvider() + + @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) @@ -1172,6 +1317,17 @@ interface WireMetroGraph { ): 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 provideTeamScope( @KaliumCoreLogic coreLogic: CoreLogic, @@ -1198,6 +1354,14 @@ interface WireMetroGraph { fun provideObserveConversationDetailsUseCase(conversationScope: ConversationScope): ObserveConversationDetailsUseCase = conversationScope.observeConversationDetails + @Provides + fun provideGetConversationUnreadEventsCountUseCase(conversationScope: ConversationScope): GetConversationUnreadEventsCountUseCase = + conversationScope.getConversationUnreadEventsCountUseCase + + @Provides + fun provideClearUsersTypingEventsUseCase(conversationScope: ConversationScope): ClearUsersTypingEventsUseCase = + conversationScope.clearUsersTypingEvents + @Provides fun provideResetMLSConversationUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @@ -1232,6 +1396,10 @@ interface WireMetroGraph { fun provideObserveUsersTypingUseCase(conversationScope: ConversationScope): ObserveUsersTypingUseCase = conversationScope.observeUsersTyping + @Provides + fun provideObserveUserListByIdUseCase(conversationScope: ConversationScope): ObserveUserListByIdUseCase = + conversationScope.observeUserListById + @Provides fun provideObserveUsersTypingInConversationUseCase( observeUsersTyping: ObserveUsersTypingUseCase, @@ -1262,6 +1430,10 @@ interface WireMetroGraph { fun provideUpdateConversationMemberRoleUseCase(conversationScope: ConversationScope): UpdateConversationMemberRoleUseCase = conversationScope.updateConversationMemberRole + @Provides + fun provideUpdateConversationReceiptModeUseCase(conversationScope: ConversationScope): UpdateConversationReceiptModeUseCase = + conversationScope.updateConversationReceiptMode + @Provides fun provideIsOneToOneConversationCreatedUseCase(conversationScope: ConversationScope): IsOneToOneConversationCreatedUseCase = conversationScope.isOneToOneConversationCreatedUseCase @@ -1494,6 +1666,54 @@ interface WireMetroGraph { 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 provideGetPaginatedFlowOfMessagesByConversationUseCase( + messageScope: MessageScope, + ): GetPaginatedFlowOfMessagesByConversationUseCase = + messageScope.getPaginatedFlowOfMessagesByConversation + + @Provides + fun provideGetPaginatedFlowOfAssetMessageByConversationIdUseCase( + messageScope: MessageScope, + ): GetPaginatedFlowOfAssetMessageByConversationIdUseCase = + messageScope.getPaginatedFlowOfAssetMessageByConversationId + + @Provides + fun provideObservePaginatedAssetImageMessages(messageScope: MessageScope): ObservePaginatedAssetImageMessages = + messageScope.observePaginatedImageAssetMessageByConversationId + @Provides fun provideObserveMessageReactionsUseCase(messageScope: MessageScope): ObserveMessageReactionsUseCase = messageScope.observeMessageReactions @@ -1502,6 +1722,87 @@ interface WireMetroGraph { fun provideObserveMessageReceiptsUseCase(messageScope: MessageScope): ObserveMessageReceiptsUseCase = messageScope.observeMessageReceipts + @Provides + fun provideObserveMessageByIdUseCase(messageScope: MessageScope): ObserveMessageByIdUseCase = + messageScope.observeMessageById + + @Provides + fun provideGetUsersForMessageUseCase( + observeUserListById: ObserveUserListByIdUseCase, + messageMapper: MessageMapper, + ): GetUsersForMessageUseCase = + GetUsersForMessageUseCase(observeUserListById, messageMapper) + + @Provides + fun provideObserveMessageForConversationUseCase( + observeMessageById: ObserveMessageByIdUseCase, + getUsersForMessage: GetUsersForMessageUseCase, + messageMapper: MessageMapper, + dispatchers: DispatcherProvider, + ): ObserveMessageForConversationUseCase = + ObserveMessageForConversationUseCase( + observeMessage = observeMessageById, + getUsersForMessage = getUsersForMessage, + messageMapper = messageMapper, + dispatchers = dispatchers, + ) + + @Provides + fun provideObserveQuoteMessageForConversationUseCase( + observeMessageById: ObserveMessageByIdUseCase, + getUsersForMessage: GetUsersForMessageUseCase, + messageMapper: MessageMapper, + dispatchers: DispatcherProvider, + ): ObserveQuoteMessageForConversationUseCase = + ObserveQuoteMessageForConversationUseCase( + observeMessageById = observeMessageById, + getUsersForMessage = getUsersForMessage, + messageMapper = messageMapper, + dispatchers = dispatchers, + ) + + @Provides + fun provideGetMessagesForConversationUseCase( + getMessages: GetPaginatedFlowOfMessagesByConversationUseCase, + getUsersForMessage: GetUsersForMessageUseCase, + messageMapper: MessageMapper, + dispatchers: DispatcherProvider, + ): GetMessagesForConversationUseCase = + GetMessagesForConversationUseCase( + getMessages = getMessages, + getUsersForMessage = getUsersForMessage, + messageMapper = messageMapper, + dispatchers = dispatchers, + ) + + @Provides + fun provideGetAssetMessagesFromConversationUseCase( + getAssetMessages: GetPaginatedFlowOfAssetMessageByConversationIdUseCase, + getUsersForMessage: GetUsersForMessageUseCase, + messageMapper: MessageMapper, + dispatchers: DispatcherProvider, + ): GetAssetMessagesFromConversationUseCase = + GetAssetMessagesFromConversationUseCase( + getAssetMessages = getAssetMessages, + getUsersForMessage = getUsersForMessage, + messageMapper = messageMapper, + dispatchers = dispatchers, + ) + + @Provides + fun provideObserveImageAssetMessagesFromConversationUseCase( + getAssetMessages: ObservePaginatedAssetImageMessages, + assetMapper: UIAssetMapper, + dispatchers: DispatcherProvider, + timeZoneProvider: TimeZoneProvider, + ): ObserveImageAssetMessagesFromConversationUseCase = + ObserveImageAssetMessagesFromConversationUseCase( + getAssetMessages = getAssetMessages, + assetMapper = assetMapper, + dispatchers = dispatchers, + timeZoneProvider = timeZoneProvider, + ) + @Provides fun provideObserveReactionsForMessageUseCase( observeMessageReactions: ObserveMessageReactionsUseCase, @@ -1541,10 +1842,48 @@ interface WireMetroGraph { 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 provideOnlineEditor(@ApplicationContext context: Context): OnlineEditor = + OnlineEditor(context) + + @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) @@ -1820,6 +2159,8 @@ interface WireMetroHiltEntryPoint { fun wireNotificationManager(): WireNotificationManager + fun conversationAudioMessagePlayer(): ConversationAudioMessagePlayer + fun locationPickerHelperFlavor(): LocationPickerHelperFlavor fun logFileWriter(): LogFileWriter 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/ui/home/conversations/CompositeMessageViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/CompositeMessageViewModel.kt index 8f2c2694dfe..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 @@ -23,16 +23,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.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 @@ -42,10 +37,9 @@ interface CompositeMessageViewModel { fun sendButtonActionMessage(buttonId: String) {} } -@HiltViewModel(assistedFactory = CompositeMessageViewModelImpl.Factory::class) -class CompositeMessageViewModelImpl @AssistedInject constructor( +class CompositeMessageViewModelImpl( private val sendButtonActionMessageUseCase: SendButtonActionMessageUseCase, - @Assisted private val scopedArgs: CompositeMessageArgs, + private val scopedArgs: CompositeMessageArgs, ) : CompositeMessageViewModel, ViewModel() { val conversationId: QualifiedID = scopedArgs.conversationId @@ -65,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/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index d96de78c11c..f002f8a03bf 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 @@ -102,6 +102,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 @@ -268,9 +269,7 @@ fun ConversationScreen( creationCallback = { factory -> factory.create(args) } ), conversationMessagesViewModel: ConversationMessagesViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { conversationMessagesViewModelFactory.create(args) }, messageComposerViewModel: MessageComposerViewModel = hiltViewModel( creationCallback = { factory -> factory.create(args) } 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 61e19a04a1e..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 @@ -148,10 +148,9 @@ fun GroupConversationDetailsScreen( conversationFoldersScreenResultRecipient: ResultRecipient, args: GroupConversationDetailsNavArgs, - viewModel: GroupConversationDetailsViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + viewModel: GroupConversationDetailsViewModel = metroViewModel { + groupConversationDetailsViewModelFactory.create(args) + }, ) { val scope = rememberCoroutineScope() val resources = LocalContext.current.resources @@ -315,6 +314,7 @@ fun GroupConversationDetailsScreen( ) ) }, + onReadReceiptSwitchClicked = viewModel::onReadReceiptUpdate, isScreenLoading = viewModel.isFetchingInitialData ) @@ -385,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), ) { @@ -537,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 f673cce3a22..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 @@ -46,12 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -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 @@ -66,9 +62,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel(assistedFactory = GroupConversationDetailsViewModel.Factory::class) -class GroupConversationDetailsViewModel @AssistedInject constructor( - @Assisted private val groupConversationDetailsNavArgs: GroupConversationDetailsNavArgs, +class GroupConversationDetailsViewModel( + private val groupConversationDetailsNavArgs: GroupConversationDetailsNavArgs, private val dispatcher: DispatcherProvider, private val observeConversationDetails: ObserveConversationDetailsUseCase, observeConversationMembers: ObserveParticipantsForConversationUseCase, @@ -98,11 +93,6 @@ class GroupConversationDetailsViewModel @AssistedInject constructor( observeConversationDetails() } - @AssistedFactory - interface Factory { - fun create(args: GroupConversationDetailsNavArgs): GroupConversationDetailsViewModel - } - private suspend fun groupDetailsFlow(): Flow = observeConversationDetails(conversationId) .filterIsInstance() .map { it.conversationDetails } 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/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/MessageOptionsModalSheetLayout.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/edit/MessageOptionsModalSheetLayout.kt index ba985d6a406..2020b78ef5c 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.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel 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,12 +62,11 @@ fun MessageOptionsModalSheetLayout( onDownloadAssetClick: (messageId: String) -> Unit, onOpenAssetClick: (messageId: String) -> Unit, viewModel: MessageOptionsMenuViewModel = - wireViewModelScoped< - MessageOptionsMenuViewModelImpl, - MessageOptionsMenuViewModel, - MessageOptionsMenuArgs, - MessageOptionsMenuViewModelImpl.Factory - >(MessageOptionsMenuArgs(conversationId)) + MessageOptionsMenuArgs(conversationId).let { args -> + metroViewModel(key = args.key) { + messageOptionsMenuViewModelFactory.create(args) + } + } ) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current 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 658a2d5fbde..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 @@ -27,17 +27,12 @@ import com.wire.android.ui.home.conversations.usecase.GetAssetMessagesFromConver import com.wire.android.ui.home.conversations.usecase.ObserveImageAssetMessagesFromConversationUseCase import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.feature.asset.ObserveAssetStatusesUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = ConversationAssetMessagesViewModel.Factory::class) @Suppress("LongParameterList", "TooManyFunctions") -class ConversationAssetMessagesViewModel @AssistedInject constructor( - @Assisted conversationMediaNavArgs: ConversationMediaNavArgs, +class ConversationAssetMessagesViewModel( + conversationMediaNavArgs: ConversationMediaNavArgs, private val getImageMessages: ObserveImageAssetMessagesFromConversationUseCase, private val getAssetMessages: GetAssetMessagesFromConversationUseCase, private val observeAssetStatuses: ObserveAssetStatusesUseCase, @@ -54,11 +49,6 @@ class ConversationAssetMessagesViewModel @AssistedInject constructor( observeAssetStatuses() } - @AssistedFactory - interface Factory { - fun create(args: ConversationMediaNavArgs): ConversationAssetMessagesViewModel - } - private fun loadAssets() = viewModelScope.launch { val assetsResult = getAssetMessages.invoke( conversationId = conversationId, 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 f941bcfa584..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 @@ -89,14 +89,12 @@ import kotlinx.serialization.Serializable fun ConversationMediaScreen( navigator: Navigator, args: ConversationMediaNavArgs, - conversationAssetMessagesViewModel: ConversationAssetMessagesViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), - conversationMessagesViewModel: ConversationMessagesViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(ConversationNavArgs(args.conversationId)) } - ) + conversationAssetMessagesViewModel: ConversationAssetMessagesViewModel = metroViewModel { + conversationAssetMessagesViewModelFactory.create(args) + }, + conversationMessagesViewModel: ConversationMessagesViewModel = metroViewModel { + conversationMessagesViewModelFactory.create(ConversationNavArgs(args.conversationId)) + } ) { val permissionPermanentlyDeniedDialogState = rememberVisibilityState() val context = LocalContext.current 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 fd36104fb2c..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 @@ -63,10 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -85,10 +81,9 @@ import okio.Path import kotlin.math.max import kotlin.time.Duration.Companion.seconds -@HiltViewModel(assistedFactory = ConversationMessagesViewModel.Factory::class) @Suppress("LongParameterList", "TooManyFunctions") -class ConversationMessagesViewModel @AssistedInject constructor( - @Assisted private val conversationNavArgs: ConversationNavArgs, +class ConversationMessagesViewModel( + private val conversationNavArgs: ConversationNavArgs, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val getMessageAsset: GetMessageAssetUseCase, private val getMessageByIdUseCase: GetMessageByIdUseCase, @@ -134,11 +129,6 @@ class ConversationMessagesViewModel @AssistedInject constructor( observeAssetStatuses() } - @AssistedFactory - interface Factory { - fun create(conversationNavArgs: ConversationNavArgs): ConversationMessagesViewModel - } - val currentTimeInMillisFlow: Flow = flow { while (true) { delay(CURRENT_TIME_REFRESH_WINDOW_IN_MILLIS) 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 d1046a77d28..b2d25d9df70 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.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset import com.wire.android.ui.common.StatusBox @@ -65,10 +65,10 @@ 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.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 +622,9 @@ private fun QuotedImageThumbnail( val keyInScopeResolver = LocalAssetLocalPathKeyInScopeResolver.current val viewModel: AssetLocalPathViewModel = if (keyInScopeResolver != null && keyInScopeResolver(args.key)) { - wireViewModelScoped< - AssetLocalPathViewModelImpl, - AssetLocalPathViewModel, - AssetLocalPathArgs, - AssetLocalPathViewModelImpl.Factory, - >( - arguments = args, - keyInScopeResolver = keyInScopeResolver, - ) + assetLocalPathViewModel(args) } else { - wireViewModelScoped< - AssetLocalPathViewModelImpl, - AssetLocalPathViewModel, - AssetLocalPathArgs, - AssetLocalPathViewModelImpl.Factory, - >( - args - ) + assetLocalPathViewModel(args) } LaunchedEffect(Unit) { @@ -673,6 +658,12 @@ private fun QuotedImageThumbnail( } } +@Composable +private fun assetLocalPathViewModel(args: AssetLocalPathArgs): AssetLocalPathViewModel = + metroViewModel(key = args.key) { + assetLocalPathViewModelFactory.create(args) + } + @Composable fun QuotedAudioMessage( senderName: UIText, 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/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 index cd10811fd1d..6a39be0e95f 100644 --- 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 @@ -22,7 +22,7 @@ import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import dev.zacsweers.metro.Inject @Inject -internal class AssetLocalPathViewModelFactory( +class AssetLocalPathViewModelFactory( private val getMessageAsset: GetMessageAssetUseCase, private val dispatchers: DispatcherProvider, ) { 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/MessageContentAndStatus.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContentAndStatus.kt index 785dab7e660..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) { 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 479280923d4..70f7cbf2351 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.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset import com.wire.android.ui.common.applyIf @@ -172,14 +172,11 @@ fun MessageButtonsContent( messageStyle: MessageStyle, modifier: Modifier = Modifier, viewModel: CompositeMessageViewModel = - wireViewModelScoped< - CompositeMessageViewModelImpl, - CompositeMessageViewModel, - CompositeMessageArgs, - CompositeMessageViewModelImpl.Factory - >( - CompositeMessageArgs(conversationId, messageId) - ) + CompositeMessageArgs(conversationId, messageId).let { args -> + metroViewModel(key = args.key) { + compositeMessageViewModelFactory.create(args) + } + } ) { 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 dcc7302e38f..d4b1d046a26 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 @@ -60,12 +60,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource 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.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel import com.wire.android.media.audiomessage.AudioMediaPlayingState import com.wire.android.media.audiomessage.AudioMessageArgs import com.wire.android.media.audiomessage.AudioMessageViewModel @@ -89,7 +90,6 @@ import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.WireCircularProgressIndicator import com.wire.android.ui.common.spacers.HorizontalSpace -import com.wire.android.ui.home.conversations.LocalAudioMessageKeyInScopeResolver import com.wire.android.ui.home.conversations.messages.item.MessageStyle import com.wire.android.ui.home.conversations.messages.item.isBubble import com.wire.android.ui.home.conversations.messages.item.playedColor @@ -180,21 +180,13 @@ private fun UploadedAudioMessage( messageStyle: MessageStyle, modifier: Modifier = Modifier, ) { - val keyInScopeResolver = LocalAudioMessageKeyInScopeResolver.current - val viewModel: AudioMessageViewModel = if (keyInScopeResolver != null) { - wireViewModelScoped< - AudioMessageViewModelImpl, - AudioMessageViewModel, - AudioMessageArgs, - AudioMessageViewModelImpl.Factory - >(audioMessageArgs, keyInScopeResolver) - } else { - wireViewModelScoped< - AudioMessageViewModelImpl, - AudioMessageViewModel, - AudioMessageArgs, - AudioMessageViewModelImpl.Factory - >(audioMessageArgs) + val viewModel: AudioMessageViewModel = when { + LocalInspectionMode.current -> remember { object : AudioMessageViewModel {} } + else -> metroViewModel( + key = audioMessageArgs.key.toString(), + ) { + audioMessageViewModelFactory.create(audioMessageArgs) + } } val sanitizedAudioState by remember(viewModel.state.audioState, audioMessageDurationInMs) { 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/messagecomposer/MessageComposeActions.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposeActions.kt index 1eeed417d3e..f15308666ec 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,7 +31,7 @@ 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.wireViewModelScoped +import com.wire.android.di.metro.metroViewModel import com.wire.android.model.ClickBlockParams import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WireSecondaryIconButton @@ -245,14 +245,13 @@ fun SelfDeletingMessageAction( conversationId: ConversationId, onButtonClicked: (SelfDeletionTimer) -> Unit, viewModel: SelfDeletingMessageActionViewModel = - wireViewModelScoped< - SelfDeletingMessageActionViewModelImpl, - SelfDeletingMessageActionViewModel, - SelfDeletingMessageActionArgs, - SelfDeletingMessageActionViewModelImpl.Factory - >( - SelfDeletingMessageActionArgs(conversationId = conversationId) - ), + SelfDeletingMessageActionArgs(conversationId = conversationId).let { args -> + metroViewModel( + key = args.key.toString(), + ) { + selfDeletingMessageActionViewModelFactory.create(args) + } + }, ) { when (val state = viewModel.state()) { SelfDeletionTimer.Disabled -> {} 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 f762ccab550..9d137cea697 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.wireViewModelScoped +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.spacers.VerticalSpace @@ -184,14 +184,11 @@ private fun InputContent( onPlusClick: () -> Unit, modifier: Modifier = Modifier, viewModel: SelfDeletingMessageActionViewModel = - wireViewModelScoped< - SelfDeletingMessageActionViewModelImpl, - SelfDeletingMessageActionViewModel, - SelfDeletingMessageActionArgs, - SelfDeletingMessageActionViewModelImpl.Factory - >( - SelfDeletingMessageActionArgs(conversationId = conversationId) - ), + SelfDeletingMessageActionArgs(conversationId = conversationId).let { args -> + metroViewModel(key = args.key) { + selfDeletingMessageActionViewModelFactory.create(args) + } + }, ) { ConstraintLayout(modifier = modifier) { val (additionalOptionButton, input, actions) = createRefs() 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 - } } From 9d10fc2c66e5024129db9d70fbe3386394ea749e Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 09:17:43 +0200 Subject: [PATCH 23/46] refactor: migrate feature ViewModels to Metro graph --- .../android/di/metro/MetroViewModelExt.kt | 7 +- .../wire/android/di/metro/WireMetroGraph.kt | 799 +++++++++++++++++- .../wire/android/navigation/MainNavHost.kt | 45 +- .../calling/common/SharedCallingViewModel.kt | 14 +- .../ui/calling/incoming/IncomingCallScreen.kt | 16 +- .../calling/incoming/IncomingCallViewModel.kt | 17 +- .../ui/calling/ongoing/OngoingCallScreen.kt | 16 +- .../calling/ongoing/OngoingCallViewModel.kt | 17 +- .../ui/calling/outgoing/OutgoingCallScreen.kt | 16 +- .../calling/outgoing/OutgoingCallViewModel.kt | 14 +- .../com/wire/android/ui/home/HomeScreen.kt | 25 +- .../home/conversations/ConversationScreen.kt | 33 +- .../banner/ConversationBannerViewModel.kt | 14 +- .../call/ConversationCallViewModel.kt | 17 +- .../composer/MessageComposerViewModel.kt | 14 +- .../info/ConversationInfoViewModel.kt | 17 +- .../ConversationListCallViewModel.kt | 5 +- .../ConversationListViewModel.kt | 19 +- .../ConversationsScreenContent.kt | 15 +- .../ImportMediaAuthenticatedViewModel.kt | 5 +- .../android/ui/sharing/ImportMediaScreen.kt | 5 +- .../userprofile/self/SelfUserProfileScreen.kt | 5 +- .../self/SelfUserProfileViewModel.kt | 5 +- core/di/build.gradle.kts | 1 + .../android/di/metro/MetroViewModelGraph.kt | 60 ++ features/cells/build.gradle.kts | 1 + .../feature/cells/ui/AllFilesScreen.kt | 4 +- .../feature/cells/ui/CellFilesScreen.kt | 10 +- .../feature/cells/ui/CellViewModelGraph.kt | 44 + .../cells/ui/ConversationFilesScreen.kt | 4 +- ...rsationFilesWithSlideInTransitionScreen.kt | 4 +- .../cells/ui/create/file/CreateFileScreen.kt | 6 +- .../ui/create/folder/CreateFolderScreen.kt | 6 +- .../ui/movetofolder/MoveToFolderScreen.kt | 6 +- .../cells/ui/publiclink/PublicLinkScreen.kt | 6 +- .../expiration/PublicLinkExpirationScreen.kt | 6 +- ...licLinkExpirationScreenViewModelFactory.kt | 4 +- .../password/PublicLinkPasswordScreen.kt | 6 +- ...ublicLinkPasswordScreenViewModelFactory.kt | 4 +- .../cells/ui/recyclebin/RecycleBinScreen.kt | 6 +- .../cells/ui/rename/RenameNodeScreen.kt | 6 +- .../feature/cells/ui/search/SearchScreen.kt | 6 +- .../cells/ui/tags/AddRemoveTagsScreen.kt | 6 +- .../ui/versioning/VersionHistoryScreen.kt | 6 +- .../feature/meetings/ui/list/MeetingList.kt | 46 +- .../meetings/ui/list/MeetingListViewModel.kt | 14 +- 46 files changed, 1099 insertions(+), 303 deletions(-) create mode 100644 core/di/src/main/kotlin/com/wire/android/di/metro/MetroViewModelGraph.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/CellViewModelGraph.kt 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 index b06090bdf30..9069e49d9a2 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelExt.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelExt.kt @@ -19,7 +19,6 @@ package com.wire.android.di.metro import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelStoreOwner @@ -28,10 +27,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory -val LocalWireMetroGraph = staticCompositionLocalOf { - null -} - @Composable inline fun metroViewModel( viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { @@ -40,7 +35,7 @@ inline fun metroViewModel( key: String? = null, crossinline create: WireMetroGraph.() -> VM, ): VM { - val providedGraph = LocalWireMetroGraph.current + val providedGraph = LocalMetroViewModelGraph.current as? WireMetroGraph val context = LocalContext.current val graph = providedGraph ?: remember(context) { createWireMetroGraph(context) } val factory = remember(graph) { 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 index ede80e67b56..1545047de34 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -38,14 +38,24 @@ import com.wire.android.di.DefaultWebSocketEnabledByDefault import com.wire.android.di.KaliumCoreLogic import com.wire.android.emm.ManagedConfigurationsManager import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.feature.DisableAppLockUseCase import com.wire.android.feature.ObserveAppLockConfigUseCase 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.CellFileActionsMenu +import com.wire.android.feature.cells.ui.CellFileExternalActions +import com.wire.android.feature.cells.ui.CellFileLocalPathCache +import com.wire.android.feature.cells.ui.OpenFileDownloadController import com.wire.android.feature.cells.ui.edit.OnlineEditor +import com.wire.android.feature.cells.util.FileHelper +import com.wire.android.feature.cells.util.FileNameResolver import com.wire.android.media.audiomessage.AudioFocusHelper import com.wire.android.media.audiomessage.AudioMessageViewModelFactory import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer import com.wire.android.media.audiomessage.RecordAudioMessagePlayer +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 @@ -54,9 +64,11 @@ 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.notification.WireNotificationManager +import com.wire.android.notification.CallNotificationManager 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 @@ -99,9 +111,22 @@ import com.wire.android.ui.home.conversations.details.editselfdeletingmessages.E 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.MessageSharedState +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.banner.usecase.ObserveConversationMembersByTypesUseCase +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 @@ -120,16 +145,20 @@ import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewM 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.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.GetAssetMessagesFromConversationUseCase import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase import com.wire.android.ui.home.conversations.usecase.GetMessagesForConversationUseCase +import com.wire.android.ui.home.conversations.usecase.GetQuoteMessageForConversationUseCase import com.wire.android.ui.home.conversations.usecase.GetUsersForMessageUseCase import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase import com.wire.android.ui.home.conversations.usecase.ObserveImageAssetMessagesFromConversationUseCase @@ -137,7 +166,13 @@ import com.wire.android.ui.home.conversations.usecase.ObserveMessageForConversat import com.wire.android.ui.home.conversations.usecase.ObserveQuoteMessageForConversationUseCase import com.wire.android.ui.home.conversations.usecase.ObserveUsersTypingInConversationUseCase 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.LocationPickerHelperFlavor @@ -173,7 +208,26 @@ 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.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.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 @@ -186,6 +240,7 @@ 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.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 @@ -199,6 +254,7 @@ 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 @@ -206,6 +262,10 @@ import com.wire.android.util.AvatarImageManager import com.wire.android.util.CurrentScreenManager import com.wire.android.util.EMPTY import com.wire.android.util.FileManager +import com.wire.android.util.FileSizeFormatter +import com.wire.android.util.GetMediaMetadataUseCase +import com.wire.android.util.GetMediaMetadataUseCaseImpl +import com.wire.android.util.ImageUtil import com.wire.android.util.ScreenStateObserver import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.isWebsocketEnabledByDefault @@ -217,12 +277,45 @@ import com.wire.android.util.ui.AndroidUiTextResolver import com.wire.android.util.ui.CountdownTimer import com.wire.android.util.ui.UiTextResolver 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 @@ -238,6 +331,7 @@ import com.wire.kalium.logic.feature.asset.GetPaginatedFlowOfAssetMessageByConve 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 @@ -258,9 +352,36 @@ 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.DeleteClientUseCase @@ -269,7 +390,9 @@ 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 @@ -295,12 +418,25 @@ import com.wire.kalium.logic.feature.conversation.GetPaginatedFlowOfConversation 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.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.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 @@ -308,6 +444,8 @@ import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusU 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 @@ -337,13 +475,16 @@ 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 @@ -351,6 +492,7 @@ import com.wire.kalium.logic.feature.session.DoesValidNomadAccountExistUseCase import com.wire.kalium.logic.feature.session.GetSessionsUseCase 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.GetTeamUrlUseCase import com.wire.kalium.logic.feature.service.GetServiceByIdUseCase import com.wire.kalium.logic.feature.service.ObserveAllServicesUseCase @@ -364,16 +506,29 @@ 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 @@ -418,10 +573,12 @@ import com.wire.kalium.logic.feature.user.webSocketStatus.PersistPersistentWebSo 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 dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.EntryPoint @@ -440,7 +597,7 @@ abstract class WireMetroScope private constructor() @DependencyGraph(WireMetroScope::class) @Suppress("LargeClass", "TooManyFunctions") -interface WireMetroGraph { +interface WireMetroGraph : CellViewModelGraph { @DependencyGraph.Factory fun interface Factory { fun create(@Provides @ApplicationContext context: Context): WireMetroGraph @@ -538,6 +695,39 @@ interface WireMetroGraph { 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 featureFlagNotificationViewModelFactory: FeatureFlagNotificationViewModelFactory + val legalHoldRequestedViewModelFactory: LegalHoldRequestedViewModelFactory val dispatcherProvider: DispatcherProvider @@ -559,6 +749,13 @@ interface WireMetroGraph { @KaliumCoreLogic fun provideKaliumCoreLogic(entryPoint: WireMetroHiltEntryPoint): CoreLogic = entryPoint.coreLogic() + @Provides + @KaliumCoreLogic + fun provideKaliumCoreLogicLazy(@KaliumCoreLogic coreLogic: CoreLogic): dagger.Lazy = + object : dagger.Lazy { + override fun get(): CoreLogic = coreLogic + } + @Provides fun provideUserDataStoreProvider(entryPoint: WireMetroHiltEntryPoint): UserDataStoreProvider = entryPoint.userDataStoreProvider() @@ -570,6 +767,18 @@ interface WireMetroGraph { ): 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(entryPoint: WireMetroHiltEntryPoint): DispatcherProvider = entryPoint.dispatcherProvider() @@ -664,6 +873,35 @@ interface WireMetroGraph { fun provideAnonymousAnalyticsManager(): AnonymousAnalyticsManager = AnonymousAnalyticsManagerImpl + @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 provideSelfServerConfigUseCaseLazy(selfServerConfig: SelfServerConfigUseCase): dagger.Lazy = + object : dagger.Lazy { + override fun get(): SelfServerConfigUseCase = selfServerConfig + } + + @Provides + fun provideDisableAppLockUseCase( + globalDataStore: GlobalDataStore, + observeIsAppLockEditable: ObserveIsAppLockEditableUseCase, + ): DisableAppLockUseCase = + DisableAppLockUseCase(globalDataStore, observeIsAppLockEditable) + + @Provides + fun provideDisableAppLockUseCaseLazy(disableAppLockUseCase: DisableAppLockUseCase): dagger.Lazy = + object : dagger.Lazy { + override fun get(): DisableAppLockUseCase = disableAppLockUseCase + } + @Provides fun provideKaliumConfigs(): KaliumConfigs = KaliumConfigs( @@ -925,6 +1163,13 @@ interface WireMetroGraph { ): 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, @@ -997,6 +1242,13 @@ interface WireMetroGraph { 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 @@ -1023,6 +1275,10 @@ interface WireMetroGraph { fun provideUserTypeMapper(): UserTypeMapper = UserTypeMapper() + @Provides + fun provideUICallParticipantMapper(userTypeMapper: UserTypeMapper): UICallParticipantMapper = + UICallParticipantMapper(userTypeMapper) + @Provides fun provideContactMapper(userTypeMapper: UserTypeMapper): ContactMapper = ContactMapper(userTypeMapper) @@ -1031,6 +1287,44 @@ interface WireMetroGraph { fun provideMessageResourceProvider(): MessageResourceProvider = MessageResourceProvider() + @Provides + fun provideMessageSharedState(): MessageSharedState = + MessageSharedState() + + @Provides + fun provideGetMediaMetadataUseCase(): GetMediaMetadataUseCase = + GetMediaMetadataUseCaseImpl() + + @Provides + fun provideImageUtil(): ImageUtil = + ImageUtil + + @Provides + fun providePingRinger(@ApplicationContext context: Context): PingRinger = + PingRinger(context) + + @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() @@ -1266,6 +1560,10 @@ interface WireMetroGraph { fun provideGetOrRegisterClientUseCase(clientScope: ClientScope): GetOrRegisterClientUseCase = clientScope.getOrRegister + @Provides + fun provideNeedsToRegisterClientUseCase(clientScope: ClientScope): NeedsToRegisterClientUseCase = + clientScope.needsToRegisterClient + @Provides fun provideFetchSelfClientsFromRemoteUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @@ -1328,6 +1626,14 @@ interface WireMetroGraph { 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, @@ -1354,6 +1660,12 @@ interface WireMetroGraph { fun provideObserveConversationDetailsUseCase(conversationScope: ConversationScope): ObserveConversationDetailsUseCase = conversationScope.observeConversationDetails + @Provides + fun provideObserveConversationListDetailsWithEventsUseCase( + conversationScope: ConversationScope, + ): ObserveConversationListDetailsWithEventsUseCase = + conversationScope.observeConversationListDetailsWithEvents + @Provides fun provideGetConversationUnreadEventsCountUseCase(conversationScope: ConversationScope): GetConversationUnreadEventsCountUseCase = conversationScope.getConversationUnreadEventsCountUseCase @@ -1362,6 +1674,86 @@ interface WireMetroGraph { 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 provideObserveConversationMembersByTypesUseCase( + observeConversationMembers: ObserveConversationMembersUseCase, + dispatchers: DispatcherProvider, + ): ObserveConversationMembersByTypesUseCase = + ObserveConversationMembersByTypesUseCase( + observeConversationMembers = observeConversationMembers, + dispatchers = dispatchers, + ) + @Provides fun provideResetMLSConversationUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @@ -1392,6 +1784,14 @@ interface WireMetroGraph { 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 @@ -1554,6 +1954,12 @@ interface WireMetroGraph { fun provideUpdateChannelAddPermissionUseCase(channelsScope: ChannelsScope): UpdateChannelAddPermissionUseCase = channelsScope.updateChannelAddPermission + @Provides + fun provideObserveChannelsCreationPermissionUseCase( + channelsScope: ChannelsScope, + ): ObserveChannelsCreationPermissionUseCase = + channelsScope.observeChannelsCreationPermissionUseCase + @Provides fun provideSendConnectionRequestUseCase(connectionScope: ConnectionScope): SendConnectionRequestUseCase = connectionScope.sendConnectionRequest @@ -1698,12 +2104,66 @@ interface WireMetroGraph { 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, @@ -1761,6 +2221,20 @@ interface WireMetroGraph { dispatchers = dispatchers, ) + @Provides + fun provideGetQuoteMessageForConversationUseCase( + getMessageById: GetMessageByIdUseCase, + getUsersForMessage: GetUsersForMessageUseCase, + messageMapper: MessageMapper, + dispatchers: DispatcherProvider, + ): GetQuoteMessageForConversationUseCase = + GetQuoteMessageForConversationUseCase( + getMessageById = getMessageById, + getUsersForMessage = getUsersForMessage, + messageMapper = messageMapper, + dispatchers = dispatchers, + ) + @Provides fun provideGetMessagesForConversationUseCase( getMessages: GetPaginatedFlowOfMessagesByConversationUseCase, @@ -1838,6 +2312,130 @@ interface WireMetroGraph { 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 @@ -1862,6 +2460,44 @@ interface WireMetroGraph { fun provideOnlineEditor(@ApplicationContext context: Context): OnlineEditor = OnlineEditor(context) + @Provides + fun provideCellsFileHelper(@ApplicationContext context: Context): FileHelper = + FileHelper(context) + + @Provides + fun provideFileNameResolver(): FileNameResolver = + FileNameResolver() + + @Provides + fun provideFileSizeFormatter(@ApplicationContext context: Context): FileSizeFormatter = + FileSizeFormatter(context) + + @Provides + fun provideCellFileExternalActions(fileHelper: FileHelper): CellFileExternalActions = + AndroidCellFileExternalActions(fileHelper) + + @Provides + fun provideCellFileActionsMenu(kaliumConfigs: KaliumConfigs): CellFileActionsMenu = + CellFileActionsMenu(kaliumConfigs) + + @Provides + fun provideCellFileLocalPathCache(): CellFileLocalPathCache = + CellFileLocalPathCache() + + @Provides + fun provideOpenFileDownloadController( + downloadCellFile: DownloadCellFileUseCase, + fileHelper: FileHelper, + fileNameResolver: FileNameResolver, + sharedPathCache: CellFileLocalPathCache, + ): OpenFileDownloadController = + OpenFileDownloadController( + download = downloadCellFile, + fileHelper = fileHelper, + fileNameResolver = fileNameResolver, + sharedPathCache = sharedPathCache, + ) + @Provides fun provideCellAssetRefreshHelper( refreshAsset: RefreshCellAssetStateUseCase, @@ -1975,9 +2611,17 @@ interface WireMetroGraph { @Provides fun provideGetConversationMessagesFromSearchUseCase( - entryPoint: WireMetroHiltEntryPoint, + getMessagesSearch: GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase, + getUsersForMessage: GetUsersForMessageUseCase, + messageMapper: MessageMapper, + dispatchers: DispatcherProvider, ): GetConversationMessagesFromSearchUseCase = - entryPoint.getConversationMessagesFromSearchUseCase() + GetConversationMessagesFromSearchUseCase( + getMessagesSearch = getMessagesSearch, + getUsersForMessage = getUsersForMessage, + messageMapper = messageMapper, + dispatchers = dispatchers, + ) @Provides fun provideObserveSelfDeletionTimerSettingsForConversationUseCase( @@ -2084,10 +2728,155 @@ interface WireMetroGraph { 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 provideCallRinger(@ApplicationContext context: Context): CallRinger = + CallRinger(context) + + @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 provideCallNotificationManager(entryPoint: WireMetroHiltEntryPoint): CallNotificationManager = + entryPoint.callNotificationManager() + @Provides fun provideNetworkSettingsDefaultsProvider( @ApplicationContext context: Context, @@ -2159,6 +2948,8 @@ interface WireMetroHiltEntryPoint { fun wireNotificationManager(): WireNotificationManager + fun callNotificationManager(): CallNotificationManager + fun conversationAudioMessagePlayer(): ConversationAudioMessagePlayer fun locationPickerHelperFlavor(): LocationPickerHelperFlavor @@ -2166,6 +2957,4 @@ interface WireMetroHiltEntryPoint { fun logFileWriter(): LogFileWriter fun accountSwitchUseCase(): AccountSwitchUseCase - - fun getConversationMessagesFromSearchUseCase(): GetConversationMessagesFromSearchUseCase } 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 d5b3af1c99d..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 @@ -49,6 +49,9 @@ 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 @@ -65,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, @@ -87,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 @@ -97,8 +109,8 @@ fun MainNavHost( } val args = NewLoginPasswordScreenDestination.argsFrom(loginPasswordEntry.arguments) dependency( - hiltViewModel(loginPasswordEntry) { factory -> - factory.create(args) + metroViewModel(loginPasswordEntry) { + loginEmailViewModelFactory.create(args) } ) } @@ -109,16 +121,13 @@ fun MainNavHost( navController.previousBackStackEntry } dependency( - hiltViewModel( - parentEntry ?: navBackStackEntry, - creationCallback = { factory -> - val searchArgs = SearchScreenDestination.argsFrom(navBackStackEntry) - factory.create( - CellFilesNavArgs(conversationId = searchArgs.conversationId), - searchArgs - ) - } - ) + metroViewModel(parentEntry ?: navBackStackEntry) { + val searchArgs = SearchScreenDestination.argsFrom(navBackStackEntry) + cellViewModelFactory.create( + CellFilesNavArgs(conversationId = searchArgs.conversationId), + searchArgs + ) + } ) } @@ -128,7 +137,11 @@ fun MainNavHost( val parentEntry = remember(navBackStackEntry) { navController.getBackStackEntry(PersonalToTeamMigrationGraph.route) } - dependency(hiltViewModel(parentEntry)) + dependency( + metroViewModel(parentEntry) { + teamMigrationViewModelFactory.create() + } + ) } }, manualComposableCallsBuilder = { 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 442a8d62e8d..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 @@ -50,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 @@ -66,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, @@ -250,11 +245,6 @@ class SharedCallingViewModel @AssistedInject constructor( 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/incoming/IncomingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt index a281f0d7e26..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 @@ -72,15 +72,13 @@ import com.wire.kalium.logic.util.PlatformView 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 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/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt index ca414d89d76..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 @@ -156,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() 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/outgoing/OutgoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt index 334bc5f76cc..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 @@ -58,14 +58,12 @@ import com.wire.kalium.logic.util.PlatformView 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 = 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/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt index 5a2fd601278..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,27 +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.CellFilesNavArgs -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 @@ -113,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 @@ -378,10 +378,9 @@ fun HomeContent( .getBackStackEntry(HomeScreenDestination.route) } dependency( - hiltViewModel( - parentEntry, - creationCallback = { factory -> factory.create(CellFilesNavArgs(), null) } - ) + metroViewModel(viewModelStoreOwner = parentEntry) { + cellViewModelFactory.create(CellFilesNavArgs(), null) + } ) } } 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 f002f8a03bf..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 @@ -79,7 +79,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.core.net.toUri -import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems @@ -257,39 +256,23 @@ fun ConversationScreen( drawingCanvasScreenResultRecipient: OpenResultRecipient, resultNavigator: ResultBackNavigator, conversationInfoViewModel: ConversationInfoViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { conversationInfoViewModelFactory.create(args) }, conversationBannerViewModel: ConversationBannerViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { conversationBannerViewModelFactory.create(args) }, conversationCallViewModel: ConversationCallViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { conversationCallViewModelFactory.create(args) }, conversationMessagesViewModel: ConversationMessagesViewModel = metroViewModel { conversationMessagesViewModelFactory.create(args) }, messageComposerViewModel: MessageComposerViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { messageComposerViewModelFactory.create(args) }, sendMessageViewModel: SendMessageViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { sendMessageViewModelFactory.create(args) }, conversationMigrationViewModel: ConversationMigrationViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { conversationMigrationViewModelFactory.create(args) }, messageDraftViewModel: MessageDraftViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { messageDraftViewModelFactory.create(args) }, messageAttachmentsViewModel: MessageAttachmentsViewModel = - hiltViewModel( - creationCallback = { factory -> factory.create(args) } - ), + metroViewModel { messageAttachmentsViewModelFactory.create(args) }, ) { val coroutineScope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current 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 62e87ebe5ad..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 @@ -36,19 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel(assistedFactory = ConversationBannerViewModel.Factory::class) -class ConversationBannerViewModel @AssistedInject constructor( - @Assisted private val conversationNavArgs: ConversationNavArgs, +class ConversationBannerViewModel( + private val conversationNavArgs: ConversationNavArgs, private val observeConversationMembersByTypes: ObserveConversationMembersByTypesUseCase, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val notifyConversationIsOpen: NotifyConversationIsOpenUseCase, @@ -75,11 +70,6 @@ class ConversationBannerViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: ConversationNavArgs): ConversationBannerViewModel - } - @Suppress("ComplexMethod") private fun handleConversationMemberTypes(userTypesInfo: Set) { val containsService = userTypesInfo.any { it.isAppOrBot() } 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 f2b4fb61f71..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 @@ -23,7 +23,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.ui.common.ActionsViewModel import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase @@ -47,10 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest @@ -59,11 +54,10 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = ConversationCallViewModel.Factory::class) @Suppress("LongParameterList", "TooManyFunctions") -class ConversationCallViewModel @AssistedInject constructor( - @Assisted private val conversationNavArgs: ConversationNavArgs, - @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, @@ -96,11 +90,6 @@ class ConversationCallViewModel @AssistedInject constructor( observeCallingActivatedEvent() } - @AssistedFactory - interface Factory { - fun create(args: ConversationNavArgs): ConversationCallViewModel - } - private fun observeCallingActivatedEvent() { viewModelScope.launch { observeConferenceCallingEnabled() 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 95403d22183..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 @@ -50,10 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged @@ -66,9 +62,8 @@ import kotlinx.coroutines.launch import kotlinx.datetime.Instant @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel(assistedFactory = MessageComposerViewModel.Factory::class) -class MessageComposerViewModel @AssistedInject constructor( - @Assisted conversationNavArgs: ConversationNavArgs, +class MessageComposerViewModel( + conversationNavArgs: ConversationNavArgs, private val dispatchers: DispatcherProvider, private val isFileSharingEnabled: IsFileSharingEnabledUseCase, private val observeConversationInteractionAvailability: ObserveConversationInteractionAvailabilityUseCase, @@ -245,9 +240,4 @@ class MessageComposerViewModel @AssistedInject constructor( messageComposerViewState.value = messageComposerViewState.value.copy(isCallOngoing = hasOngoingCalls) } } - - @AssistedFactory - interface Factory { - fun create(args: ConversationNavArgs): MessageComposerViewModel - } } 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 889d87350a9..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 @@ -25,7 +25,6 @@ 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.wire.android.util.ui.UIText @@ -39,21 +38,16 @@ 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel(assistedFactory = ConversationInfoViewModel.Factory::class) -class ConversationInfoViewModel @AssistedInject constructor( - @Assisted private val conversationNavArgs: ConversationNavArgs, +class ConversationInfoViewModel( + private val conversationNavArgs: ConversationNavArgs, private val qualifiedIdMapper: QualifiedIdMapper, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val fetchConversationMLSVerificationStatus: FetchConversationMLSVerificationStatusUseCase, private val isWireCellFeatureEnabled: IsWireCellsEnabledUseCase, - @CurrentAccount private val selfUserId: UserId, + private val selfUserId: UserId, ) : ViewModel() { val conversationId: QualifiedID = conversationNavArgs.conversationId @@ -63,11 +57,6 @@ class ConversationInfoViewModel @AssistedInject constructor( fetchMLSVerificationStatus() } - @AssistedFactory - interface Factory { - fun create(args: ConversationNavArgs): ConversationInfoViewModel - } - private fun fetchMLSVerificationStatus() { viewModelScope.launch { fetchConversationMLSVerificationStatus(conversationId) 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/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/sharing/ImportMediaAuthenticatedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt index 8b70b796075..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 @@ -42,7 +42,6 @@ import com.wire.kalium.logic.data.message.SelfDeletionTimer.Companion.SELF_DELET 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 @@ -57,12 +56,10 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -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 importMediaAssetImporter: ImportMediaAssetImporter, 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 d2a6813e511..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,7 +55,6 @@ 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 @@ -125,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 -> { 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 429753c2994..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,7 +50,6 @@ 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 @@ -119,7 +118,9 @@ fun SelfUserProfileScreen( loginTypeSelector: LoginTypeSelector, avatarPickerResultRecipient: ResultRecipient, viewModelSelf: SelfUserProfileViewModel = metroViewModel { selfUserProfileViewModelFactory.create() }, - legalHoldRequestedViewModel: LegalHoldRequestedViewModel = hiltViewModel() + 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/core/di/build.gradle.kts b/core/di/build.gradle.kts index 3aafaace2d4..07ae41d5a10 100644 --- a/core/di/build.gradle.kts +++ b/core/di/build.gradle.kts @@ -8,4 +8,5 @@ dependencies { implementation(libs.androidx.core) implementation(libs.hilt.android) implementation(libs.compose.material3) + implementation(libs.androidx.lifecycle.viewModelCompose) } 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/features/cells/build.gradle.kts b/features/cells/build.gradle.kts index 9c466ee014f..051d9008f62 100644 --- a/features/cells/build.gradle.kts +++ b/features/cells/build.gradle.kts @@ -14,6 +14,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) 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 75b63caff5e..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 @@ -43,8 +43,8 @@ import com.wire.android.ui.common.topappbar.search.SearchTopBar fun AllFilesScreen( navigator: WireNavigator, modifier: Modifier = Modifier, - viewModel: CellViewModel = wireCellsViewModel( - creationCallback = { factory -> factory.create(CellFilesNavArgs(), null) } + viewModel: CellViewModel = cellsMetroViewModel( + creationCallback = { cellViewModelFactory.create(CellFilesNavArgs(), null) } ), ) { 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 cde06706769..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,12 +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.hilt.navigation.compose.hiltViewModel 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 @@ -59,12 +59,12 @@ import com.wire.android.ui.common.typography import com.wire.android.ui.theme.WireTheme @Composable -internal inline fun wireCellsViewModel( +internal inline fun cellsMetroViewModel( key: String? = null, - noinline creationCallback: (VMF) -> VM, -): VM = hiltViewModel( + noinline creationCallback: CellViewModelGraph.() -> VM, +): VM = metroViewModel( key = key, - creationCallback = creationCallback + create = creationCallback, ) @OptIn(ExperimentalMaterial3Api::class) 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 d4d2a5d0c22..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 @@ -101,8 +101,8 @@ fun ConversationFilesScreen( navigator: WireNavigator, animatedVisibilityScope: AnimatedVisibilityScope, args: CellFilesNavArgs, - viewModel: CellViewModel = wireCellsViewModel( - creationCallback = { factory -> factory.create(args, null) } + viewModel: CellViewModel = cellsMetroViewModel( + creationCallback = { cellViewModelFactory.create(args, null) } ), ) { ConversationFilesScreenContent( 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 817b0dde79d..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 @@ -40,8 +40,8 @@ fun ConversationFilesWithSlideInTransitionScreen( navigator: WireNavigator, cellFilesNavArgs: CellFilesNavArgs, animatedVisibilityScope: AnimatedVisibilityScope, - viewModel: CellViewModel = wireCellsViewModel( - creationCallback = { factory -> factory.create(cellFilesNavArgs, null) } + viewModel: CellViewModel = cellsMetroViewModel( + creationCallback = { cellViewModelFactory.create(cellFilesNavArgs, null) } ), ) { LaunchedEffect(viewModel.navigateToRecycleBinRoot.collectAsState().value) { 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 6fc786a101f..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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 @@ -65,8 +65,8 @@ fun CreateFileScreen( resultNavigator: ResultBackNavigator, args: CreateFileScreenNavArgs, modifier: Modifier = Modifier, - createFileViewModel: CreateFileViewModel = wireCellsViewModel( - creationCallback = { factory -> factory.create(args) } + createFileViewModel: CreateFileViewModel = cellsMetroViewModel( + creationCallback = { createFileViewModelFactory.create(args) } ) ) { val showErrorDialog = remember { mutableStateOf(false) } 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 c8dbebc90dc..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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 @@ -71,8 +71,8 @@ fun CreateFolderScreen( args: CreateFolderScreenNavArgs, modifier: Modifier = Modifier, createFolderViewModel: CreateFolderViewModel = - wireCellsViewModel( - creationCallback = { factory -> factory.create(args) } + cellsMetroViewModel( + creationCallback = { createFolderViewModelFactory.create(args) } ) ) { val showErrorDialog = remember { mutableStateOf(false) } 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 10dca795c55..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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 @@ -81,8 +81,8 @@ fun MoveToFolderScreen( args: MoveToFolderNavArgs, modifier: Modifier = Modifier, moveToFolderViewModel: MoveToFolderViewModel = - wireCellsViewModel( - creationCallback = { factory -> factory.create(args) } + cellsMetroViewModel( + creationCallback = { moveToFolderViewModelFactory.create(args) } ) ) { val context = LocalContext.current 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 891fc86c765..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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 @@ -83,8 +83,8 @@ fun PublicLinkScreen( onExpirationChange: ResultRecipient, args: PublicLinkNavArgs, modifier: Modifier = Modifier, - viewModel: PublicLinkViewModel = wireCellsViewModel( - creationCallback = { factory -> factory.create(args) } + viewModel: PublicLinkViewModel = cellsMetroViewModel( + creationCallback = { publicLinkViewModelFactory.create(args) } ), ) { 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 3266ac2d814..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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 @@ -74,8 +74,8 @@ internal fun PublicLinkExpirationScreen( args: PublicLinkExpirationScreenNavArgs, modifier: Modifier = Modifier, viewModel: PublicLinkExpirationScreenViewModel = - wireCellsViewModel( - creationCallback = { factory -> factory.create(args) } + cellsMetroViewModel( + creationCallback = { publicLinkExpirationScreenViewModelFactory.create(args) } ), ) { 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 index 5d69a7ffcb0..807cd3e678e 100644 --- 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 @@ -21,10 +21,10 @@ import com.wire.kalium.cells.domain.usecase.publiclink.SetPublicLinkExpirationUs import dev.zacsweers.metro.Inject @Inject -internal class PublicLinkExpirationScreenViewModelFactory( +class PublicLinkExpirationScreenViewModelFactory( private val setExpiration: SetPublicLinkExpirationUseCase, ) { - fun create(navArgs: PublicLinkExpirationScreenNavArgs): PublicLinkExpirationScreenViewModel = + 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 4fbfdbc6cd2..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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,8 @@ internal fun PublicLinkPasswordScreen( args: PublicLinkPasswordNavArgs, modifier: Modifier = Modifier, viewModel: PublicLinkPasswordScreenViewModel = - wireCellsViewModel( - creationCallback = { factory -> factory.create(args) } + cellsMetroViewModel( + creationCallback = { publicLinkPasswordScreenViewModelFactory.create(args) } ), ) { val state by viewModel.state.collectAsState() 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 index 250607ef96a..878797cce1a 100644 --- 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 @@ -24,13 +24,13 @@ import com.wire.kalium.logic.util.RandomPassword import dev.zacsweers.metro.Inject @Inject -internal class PublicLinkPasswordScreenViewModelFactory( +class PublicLinkPasswordScreenViewModelFactory( private val generateRandomPassword: RandomPassword, private val createPassword: CreatePublicLinkPasswordUseCase, private val updatePassword: UpdatePublicLinkPasswordUseCase, private val getPublicLinkPassword: GetPublicLinkPasswordUseCase, ) { - fun create(navArgs: PublicLinkPasswordNavArgs): PublicLinkPasswordScreenViewModel = + internal fun create(navArgs: PublicLinkPasswordNavArgs): PublicLinkPasswordScreenViewModel = PublicLinkPasswordScreenViewModel( navArgs = navArgs, generateRandomPassword = generateRandomPassword, 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 a0e78d45d3e..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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 @@ -56,8 +56,8 @@ fun RecycleBinScreen( navigator: WireNavigator, args: CellFilesNavArgs, modifier: Modifier = Modifier, - cellViewModel: CellViewModel = wireCellsViewModel( - creationCallback = { factory -> factory.create(args, null) } + cellViewModel: CellViewModel = cellsMetroViewModel( + creationCallback = { cellViewModelFactory.create(args, null) } ) ) { 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 72c24d3c18e..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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 @@ -66,8 +66,8 @@ fun RenameNodeScreen( navigator: WireNavigator, args: RenameNodeNavArgs, modifier: Modifier = Modifier, - renameNodeViewModel: RenameNodeViewModel = wireCellsViewModel( - creationCallback = { factory -> factory.create(args) } + renameNodeViewModel: RenameNodeViewModel = cellsMetroViewModel( + creationCallback = { renameNodeViewModelFactory.create(args) } ) ) { val context = LocalContext.current 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 463079c5910..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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 @@ -78,8 +78,8 @@ fun SearchScreen( args: SearchNavArgs, modifier: Modifier = Modifier, searchScreenViewModel: SearchScreenViewModel = - wireCellsViewModel( - creationCallback = { factory -> factory.create(args) } + 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/tags/AddRemoveTagsScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/tags/AddRemoveTagsScreen.kt index 4fc32b44d89..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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 @@ -80,8 +80,8 @@ fun AddRemoveTagsScreen( args: AddRemoveTagsNavArgs, modifier: Modifier = Modifier, addRemoveTagsViewModel: AddRemoveTagsViewModel = - wireCellsViewModel( - creationCallback = { factory -> factory.create(args) } + cellsMetroViewModel( + creationCallback = { addRemoveTagsViewModelFactory.create(args) } ), ) { val context = LocalContext.current 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 9b8ea4d6e93..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 com.wire.android.feature.cells.ui.wireCellsViewModel +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 @@ -74,8 +74,8 @@ fun VersionHistoryScreen( args: VersionHistoryNavArgs, modifier: Modifier = Modifier, versionHistoryViewModel: VersionHistoryViewModel = - wireCellsViewModel( - creationCallback = { factory -> factory.create(args) } + cellsMetroViewModel( + creationCallback = { versionHistoryViewModelFactory.create(args) } ) ) { val optionsBottomSheetState = rememberWireModalSheetState>() 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..1b899633344 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 @@ -23,10 +23,16 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember 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.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 import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -43,6 +49,7 @@ import com.wire.android.ui.common.rowitem.EmptyListArrowFooter import com.wire.android.ui.common.rowitem.EmptyListContent import com.wire.android.ui.common.rowitem.LoadingListContent import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.dispatchers.DefaultDispatcherProvider @Composable fun MeetingList( @@ -52,12 +59,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() @@ -181,3 +185,31 @@ fun MeetingListNextPreview() = WireTheme { fun MeetingListPastPreview() = WireTheme { MeetingList(type = MeetingsTabItem.PAST) } + +private class MeetingListMetroFactories { + val meetingListViewModelFactory = MeetingListViewModelFactory(DefaultDispatcherProvider()) +} + +@Composable +private inline fun metroViewModel( + viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + }, + key: String? = null, + crossinline create: MeetingListMetroFactories.() -> VM, +): VM { + val factories = remember { MeetingListMetroFactories() } + val factory = remember(factories) { + viewModelFactory { + initializer { + factories.create() + } + } + } + return viewModel( + modelClass = VM::class, + viewModelStoreOwner = viewModelStoreOwner, + key = key, + factory = factory, + ) +} 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 From 876863417e0f61831ad119c39ddbcd4e8704b16c Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 10:35:22 +0200 Subject: [PATCH 24/46] refactor: migrate remaining screen ViewModels to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 270 +++++++++++++++++- .../ui/CallFeedbackViewModelFactory.kt | 2 - .../com/wire/android/ui/WireActivity.kt | 38 ++- .../ui/WireActivityViewModelFactory.kt | 2 - .../ui/analytics/AnalyticsUsageViewModel.kt | 5 +- .../create/code/CreateAccountCodeViewModel.kt | 14 +- .../details/CreateAccountDetailsViewModel.kt | 14 +- .../email/CreateAccountEmailViewModel.kt | 14 +- .../CreateAccountOverviewViewModel.kt | 14 +- .../summary/CreateAccountSummaryViewModel.kt | 14 +- .../CreateAccountUsernameViewModel.kt | 5 +- .../devices/common/ClearSessionViewModel.kt | 5 +- .../register/RegisterDeviceViewModel.kt | 5 +- .../devices/remove/RemoveDeviceViewModel.kt | 5 +- .../login/email/LoginEmailViewModel.kt | 14 +- .../login/sso/LoginSSOViewModel.kt | 13 +- .../welcome/WelcomeViewModel.kt | 14 +- .../wire/android/ui/calling/CallActivity.kt | 26 +- .../calling/CallActivityViewModelFactory.kt | 2 - .../topappbar/CommonTopAppBarViewModel.kt | 14 +- .../ConnectionActionButtonViewModel.kt | 15 +- .../ui/debug/LogManagementViewModel.kt | 5 +- .../android/ui/debug/UserDebugViewModel.kt | 6 +- .../DebugConversationViewModel.kt | 14 +- .../ConversationCryptoStatsViewModel.kt | 5 +- .../DebugFeatureFlagsViewModel.kt | 5 +- .../e2eiEnrollment/E2EIEnrollmentViewModel.kt | 5 +- .../GetE2EICertificateViewModel.kt | 5 +- .../wire/android/ui/home/AppSyncViewModel.kt | 5 +- .../com/wire/android/ui/home/HomeViewModel.kt | 5 +- .../forgot/ForgotLockScreenViewModel.kt | 5 +- .../appLock/set/SetLockScreenViewModel.kt | 5 +- .../AppUnlockWithBiometricsViewModel.kt | 5 +- .../unlock/EnterLockScreenViewModel.kt | 5 +- .../attachment/MessageAttachmentsViewModel.kt | 14 +- .../folder/NewFolderViewModel.kt | 5 +- .../media/CheckAssetRestrictionsViewModel.kt | 5 +- .../media/preview/ImagesPreviewViewModel.kt | 14 +- .../messages/draft/MessageDraftViewModel.kt | 14 +- .../ConversationMigrationViewModel.kt | 14 +- .../sendmessage/SendMessageViewModel.kt | 14 +- .../ui/home/drawer/HomeDrawerViewModel.kt | 5 +- .../ui/home/gallery/MediaGalleryViewModel.kt | 14 +- .../NewConversationViewModel.kt | 5 +- .../ui/home/settings/SettingsViewModel.kt | 5 +- .../dependencies/DependenciesViewModel.kt | 5 +- .../about/licenses/LicensesViewModel.kt | 5 +- .../settings/account/MyAccountViewModel.kt | 5 +- .../account/color/ChangeUserColorViewModel.kt | 5 +- .../deleteAccount/DeleteAccountViewModel.kt | 5 +- .../displayname/ChangeDisplayNameViewModel.kt | 5 +- .../email/updateEmail/ChangeEmailViewModel.kt | 5 +- .../email/verifyEmail/VerifyEmailViewModel.kt | 14 +- .../account/handle/ChangeHandleViewModel.kt | 5 +- .../appearance/CustomizationViewModel.kt | 5 +- .../NetworkSettingsViewModel.kt | 6 +- .../backup/BackupAndRestoreViewModel.kt | 5 +- .../privacy/PrivacySettingsViewModel.kt | 5 +- .../sync/FeatureFlagNotificationViewModel.kt | 5 +- .../ui/home/whatsnew/WhatsNewViewModel.kt | 5 +- .../ui/initialsync/InitialSyncViewModel.kt | 5 +- .../LegalHoldDeactivatedViewModel.kt | 5 +- .../requested/LegalHoldRequestedViewModel.kt | 5 +- .../login/NewLoginViewModel.kt | 42 --- .../CreateAccountVerificationCodeViewModel.kt | 20 +- .../CreateAccountDataDetailViewModel.kt | 17 +- .../CreateAccountSelectorViewModel.kt | 14 +- .../settings/about/AboutThisAppViewModel.kt | 5 +- .../devices/DeviceDetailsViewModel.kt | 14 +- .../settings/devices/SelfDevicesViewModel.kt | 5 +- .../e2ei/E2eiCertificateDetailsViewModel.kt | 14 +- .../avatarpicker/AvatarPickerViewModel.kt | 5 +- .../other/OtherUserProfileScreenViewModel.kt | 14 +- .../ui/userprofile/qr/SelfQRCodeViewModel.kt | 14 +- .../teammigration/TeamMigrationViewModel.kt | 5 +- core/ui-common/build.gradle.kts | 2 + .../com/wire/android/model/ImageAsset.kt | 20 +- .../android/feature/cells/ui/CellViewModel.kt | 16 +- .../ui/create/file/CreateFileViewModel.kt | 14 +- .../ui/create/folder/CreateFolderViewModel.kt | 14 +- .../ui/movetofolder/MoveToFolderViewModel.kt | 14 +- .../ui/publiclink/PublicLinkViewModel.kt | 14 +- .../PublicLinkExpirationScreenViewModel.kt | 14 +- .../PublicLinkPasswordScreenViewModel.kt | 14 +- .../cells/ui/rename/RenameNodeViewModel.kt | 14 +- .../cells/ui/search/SearchScreenViewModel.kt | 14 +- .../cells/ui/tags/AddRemoveTagsViewModel.kt | 14 +- .../ui/versioning/VersionHistoryViewModel.kt | 14 +- features/meetings/build.gradle.kts | 1 + .../meetings/ui/MeetingViewModelGraph.kt | 27 ++ .../feature/meetings/ui/list/MeetingList.kt | 40 +-- .../ui/options/MeetingOptionsMenuViewModel.kt | 5 +- .../options/MeetingOptionsModalSheetLayout.kt | 39 +-- 93 files changed, 495 insertions(+), 753 deletions(-) create mode 100644 features/meetings/src/main/java/com/wire/android/feature/meetings/ui/MeetingViewModelGraph.kt 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 index 1545047de34..8f382b3cb94 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -35,7 +35,12 @@ 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.ManagedConfigurationsManager import com.wire.android.feature.AccountSwitchUseCase import com.wire.android.feature.DisableAppLockUseCase @@ -67,8 +72,14 @@ 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.services.ServicesManager +import com.wire.android.sync.MonitorSyncWorkUseCase +import com.wire.android.ui.AndroidWireActivityIntentGateway +import com.wire.android.ui.WireActivityIntentGateway 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 @@ -99,6 +110,7 @@ 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.LockCodeTimeManager import com.wire.android.ui.home.appLock.set.SetLockScreenViewModelFactory @@ -210,6 +222,7 @@ 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 @@ -223,6 +236,9 @@ 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 @@ -240,6 +256,7 @@ 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 @@ -260,6 +277,7 @@ 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.FileSizeFormatter @@ -270,12 +288,15 @@ import com.wire.android.util.ScreenStateObserver 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.logging.LogFileWriter 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.kalium.cells.CellsScope import com.wire.kalium.cells.domain.CellUploadManager import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCase @@ -324,8 +345,10 @@ 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 @@ -384,6 +407,7 @@ 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 @@ -396,6 +420,7 @@ 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 @@ -489,10 +514,13 @@ 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.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 @@ -597,7 +625,7 @@ abstract class WireMetroScope private constructor() @DependencyGraph(WireMetroScope::class) @Suppress("LargeClass", "TooManyFunctions") -interface WireMetroGraph : CellViewModelGraph { +interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAssetViewModelGraph { @DependencyGraph.Factory fun interface Factory { fun create(@Provides @ApplicationContext context: Context): WireMetroGraph @@ -726,8 +754,13 @@ interface WireMetroGraph : CellViewModelGraph { val sharedCallingViewModelFactory: SharedCallingViewModelFactory val conversationListViewModelFactory: ConversationListViewModelFactory val conversationListCallViewModelFactory: ConversationListCallViewModelFactory + val commonTopAppBarViewModelFactory: CommonTopAppBarViewModelFactory val featureFlagNotificationViewModelFactory: FeatureFlagNotificationViewModelFactory val legalHoldRequestedViewModelFactory: LegalHoldRequestedViewModelFactory + val legalHoldDeactivatedViewModelFactory: LegalHoldDeactivatedViewModelFactory + override val meetingListViewModelFactory: MeetingListViewModelFactory + override val meetingOptionsMenuViewModelFactory: MeetingOptionsMenuViewModelFactory + override val imageAssetViewModelFactory: ImageAssetViewModelFactory val dispatcherProvider: DispatcherProvider @@ -883,6 +916,218 @@ interface WireMetroGraph : CellViewModelGraph { 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 provideServicesManager(entryPoint: WireMetroHiltEntryPoint): ServicesManager = + entryPoint.servicesManager() + + @Provides + fun provideServicesManagerLazy(servicesManager: ServicesManager): dagger.Lazy = + object : dagger.Lazy { + override fun get(): ServicesManager = servicesManager + } + + @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 { @@ -988,6 +1233,10 @@ interface WireMetroGraph : CellViewModelGraph { fun provideGetAvatarAssetUseCase(userScope: UserScope): GetAvatarAssetUseCase = userScope.getPublicAsset + @Provides + fun provideDeleteAssetUseCase(userScope: UserScope): DeleteAssetUseCase = + userScope.deleteAsset + @Provides fun provideUploadUserAvatarUseCase(userScope: UserScope): UploadUserAvatarUseCase = userScope.uploadUserAvatar @@ -2846,6 +3095,22 @@ interface WireMetroGraph : CellViewModelGraph { 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 fun provideCallRinger(@ApplicationContext context: Context): CallRinger = CallRinger(context) @@ -2932,6 +3197,7 @@ fun createWireMetroGraph(context: Context): WireMetroGraph = @EntryPoint @InstallIn(SingletonComponent::class) +@Suppress("TooManyFunctions") interface WireMetroHiltEntryPoint { @KaliumCoreLogic fun coreLogic(): CoreLogic @@ -2957,4 +3223,6 @@ interface WireMetroHiltEntryPoint { fun logFileWriter(): LogFileWriter fun accountSwitchUseCase(): AccountSwitchUseCase + + fun servicesManager(): ServicesManager } diff --git a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModelFactory.kt index ff64bb058c1..d01c2b72d95 100644 --- a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModelFactory.kt @@ -23,9 +23,7 @@ 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, 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 9e9762d878e..ae673c16444 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -49,8 +49,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,7 +76,8 @@ 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.di.metro.WireMetroGraph +import com.wire.android.di.metro.createWireMetroGraph import com.wire.android.emm.ManagedConfigurationsManager import com.wire.android.feature.NavigationSwitchAccountActions import com.wire.android.navigation.BackStackMode @@ -157,15 +162,27 @@ class WireActivity : BaseActivity() { @Inject lateinit var managedConfigurationsManager: ManagedConfigurationsManager + private val metroGraph by lazy(LazyThreadSafetyMode.NONE) { + createWireMetroGraph(this) + } + private val viewModel: WireActivityViewModel by viewModels() - private val featureFlagNotificationViewModel: FeatureFlagNotificationViewModel by viewModels() + private val featureFlagNotificationViewModel: FeatureFlagNotificationViewModel by metroActivityViewModel { + featureFlagNotificationViewModelFactory.create() + } private val callFeedbackViewModel: CallFeedbackViewModel by viewModels() - 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 @@ -724,6 +741,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/WireActivityViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt index e1bb7d981c2..85782054d52 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt @@ -44,9 +44,7 @@ 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, 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/authentication/create/code/CreateAccountCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt index 4f14d0604e4..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 @@ -46,17 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch // TODO: Cover this viewModel with unit test -@HiltViewModel(assistedFactory = CreateAccountCodeViewModel.Factory::class) -class CreateAccountCodeViewModel @AssistedInject constructor( - @Assisted val createAccountNavArgs: CreateAccountNavArgs, +class CreateAccountCodeViewModel constructor( + val createAccountNavArgs: CreateAccountNavArgs, @KaliumCoreLogic private val coreLogic: CoreLogic, private val addAuthenticatedUser: AddAuthenticatedUserUseCase, private val clientScopeProviderFactory: ClientScopeProvider.Factory, @@ -79,11 +74,6 @@ class CreateAccountCodeViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: CreateAccountNavArgs): CreateAccountCodeViewModel - } - fun resendCode() { codeState = codeState.copy(loading = true) viewModelScope.launch { 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 c8156b8df1c..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 @@ -28,17 +28,12 @@ import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs import com.wire.android.ui.common.textfield.textAsFlow import com.wire.kalium.logic.configuration.server.ServerConfig import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch // TODO: Cover this viewModel with unit test -@HiltViewModel(assistedFactory = CreateAccountDetailsViewModel.Factory::class) -class CreateAccountDetailsViewModel @AssistedInject constructor( - @Assisted val createAccountNavArgs: CreateAccountNavArgs, +class CreateAccountDetailsViewModel constructor( + val createAccountNavArgs: CreateAccountNavArgs, private val validatePasswordUseCase: ValidatePasswordUseCase, defaultServerConfig: ServerConfig.Links ) : ViewModel() { @@ -72,11 +67,6 @@ class CreateAccountDetailsViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: CreateAccountNavArgs): CreateAccountDetailsViewModel - } - fun onDetailsContinue() { detailsState = detailsState.copy(loading = true, continueEnabled = false) viewModelScope.launch { 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 eca0258ecf6..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 @@ -31,17 +31,12 @@ 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch // TODO: Cover this viewModel with unit test -@HiltViewModel(assistedFactory = CreateAccountEmailViewModel.Factory::class) -class CreateAccountEmailViewModel @AssistedInject constructor( - @Assisted val createAccountNavArgs: CreateAccountNavArgs, +class CreateAccountEmailViewModel constructor( + val createAccountNavArgs: CreateAccountNavArgs, private val validateEmail: ValidateEmailUseCase, @KaliumCoreLogic private val coreLogic: CoreLogic, defaultServerConfig: ServerConfig.Links @@ -66,11 +61,6 @@ class CreateAccountEmailViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: CreateAccountNavArgs): CreateAccountEmailViewModel - } - fun onEmailContinue() { emailState = emailState.copy(loading = true, continueEnabled = false) viewModelScope.launch { 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 97b0eae1139..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 @@ -19,21 +19,11 @@ package com.wire.android.ui.authentication.create.overview import androidx.lifecycle.ViewModel import com.wire.kalium.logic.configuration.server.ServerConfig -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel -@HiltViewModel(assistedFactory = CreateAccountOverviewViewModel.Factory::class) -class CreateAccountOverviewViewModel @AssistedInject constructor( - @Assisted val navArgs: CreateAccountOverviewNavArgs, +class CreateAccountOverviewViewModel constructor( + val navArgs: CreateAccountOverviewNavArgs, defaultServerConfig: ServerConfig.Links ) : ViewModel() { val serverConfig: ServerConfig.Links = navArgs.customServerConfig ?: defaultServerConfig fun learnMoreUrl(): String = serverConfig.pricing - - @AssistedFactory - interface Factory { - fun create(args: CreateAccountOverviewNavArgs): CreateAccountOverviewViewModel - } } 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 62c19412dbc..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 @@ -23,23 +23,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import com.wire.android.ui.authentication.create.common.CreateAccountFlowType -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel -@HiltViewModel(assistedFactory = CreateAccountSummaryViewModel.Factory::class) -class CreateAccountSummaryViewModel @AssistedInject constructor( - @Assisted createAccountSummaryNavArgs: CreateAccountSummaryNavArgs +class CreateAccountSummaryViewModel constructor( + createAccountSummaryNavArgs: CreateAccountSummaryNavArgs ) : ViewModel() { private val type: CreateAccountFlowType = createAccountSummaryNavArgs.type var summaryState: CreateAccountSummaryViewState by mutableStateOf(CreateAccountSummaryViewState(type)) private set - - @AssistedFactory - interface Factory { - fun create(args: CreateAccountSummaryNavArgs): CreateAccountSummaryViewModel - } } 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/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/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/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/login/email/LoginEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt index 07defed5d7c..0273ee26fa7 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 @@ -58,10 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest @@ -73,9 +69,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Suppress("LongParameterList", "ComplexMethod", "TooManyFunctions") -@HiltViewModel(assistedFactory = LoginEmailViewModel.Factory::class) -class LoginEmailViewModel @AssistedInject constructor( - @Assisted val loginNavArgs: LoginNavArgs, +class LoginEmailViewModel constructor( + val loginNavArgs: LoginNavArgs, private val addAuthenticatedUser: AddAuthenticatedUserUseCase, clientScopeProviderFactory: ClientScopeProvider.Factory, private val savedInputStore: LoginSavedInputStore, @@ -108,11 +103,6 @@ class LoginEmailViewModel @AssistedInject constructor( @VisibleForTesting internal val loginJobData = MutableStateFlow(null) - @AssistedFactory - interface Factory { - fun create(args: LoginNavArgs): LoginEmailViewModel - } - init { userIdentifierTextState.setTextAndPlaceCursorAtEnd( if (preFilledUserIdentifier is PreFilledUserIdentifierType.PreFilled) { 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 8e063c60648..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 @@ -57,10 +57,6 @@ 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest @@ -69,7 +65,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel(assistedFactory = LoginSSOViewModel.Factory::class) class LoginSSOViewModel : LoginViewModel { private val savedInputStore: LoginSavedInputStore val addAuthenticatedUser: AddAuthenticatedUserUseCase @@ -107,9 +102,8 @@ class LoginSSOViewModel : LoginViewModel { dispatchers, ) - @AssistedInject constructor( - @Assisted loginNavArgs: LoginNavArgs, + loginNavArgs: LoginNavArgs, savedInputStore: LoginSavedInputStore, addAuthenticatedUser: AddAuthenticatedUserUseCase, validateEmailUseCase: ValidateEmailUseCase, @@ -168,11 +162,6 @@ class LoginSSOViewModel : LoginViewModel { val ssoTextState: TextFieldState = TextFieldState() var loginState: LoginSSOState by mutableStateOf(LoginSSOState()) - @AssistedFactory - interface Factory { - fun create(args: LoginNavArgs): LoginSSOViewModel - } - private fun observeSSOCodeInput() { ssoTextState.setTextAndPlaceCursorAtEnd(savedInputStore.ssoCode ?: String.EMPTY) viewModelScope.launch { 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 0c1e7ca8527..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 @@ -29,15 +29,10 @@ 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = WelcomeViewModel.Factory::class) -class WelcomeViewModel @AssistedInject constructor( - @Assisted navArgs: WelcomeNavArgs, +class WelcomeViewModel constructor( + navArgs: WelcomeNavArgs, private val getSessions: GetSessionsUseCase, private val doesValidNomadAccountExist: DoesValidNomadAccountExistUseCase, defaultServerConfig: ServerConfig.Links @@ -50,11 +45,6 @@ class WelcomeViewModel @AssistedInject constructor( checkNumberOfSessions() } - @AssistedFactory - interface Factory { - fun create(args: WelcomeNavArgs): WelcomeViewModel - } - private fun checkNumberOfSessions() { viewModelScope.launch { if (doesValidNomadAccountExist()) { 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..5f15c3f4cff 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 @@ -36,9 +36,14 @@ 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.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 @@ -64,8 +69,14 @@ abstract class CallActivity : BaseActivity() { @Inject lateinit var proximitySensorManager: ProximitySensorManager - private val commonTopAppBarViewModel by assistedViewModels { factory -> - factory.create(CommonTopAppBarParams(showNoNetwork = true, showSync = false, showActiveCalls = false)) + private val metroGraph by lazy(LazyThreadSafetyMode.NONE) { + createWireMetroGraph(this) + } + + private val commonTopAppBarViewModel: CommonTopAppBarViewModel by metroActivityViewModel { + commonTopAppBarViewModelFactory.create( + CommonTopAppBarParams(showNoNetwork = true, showSync = false, showActiveCalls = false) + ) } companion object { @@ -188,4 +199,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/CallActivityViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModelFactory.kt index 3efcd51536a..8307b0a71dc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModelFactory.kt @@ -21,9 +21,7 @@ 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, 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/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/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/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/conversation/DebugConversationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/conversation/DebugConversationViewModel.kt index c7d74c59e38..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 @@ -31,23 +31,18 @@ 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.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.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = DebugConversationViewModel.Factory::class) -class DebugConversationViewModel @AssistedInject constructor( +class DebugConversationViewModel( private val conversationDetails: ObserveConversationDetailsUseCase, private val resetMLSConversation: ResetMLSConversationUseCase, private val fetchConversation: FetchConversationUseCase, private val feedConversation: DebugFeedConversationUseCase, private val getConversationEpochFromCC: GetConversationEpochFromCCUseCase, - @Assisted args: DebugConversationScreenNavArgs, + args: DebugConversationScreenNavArgs, ) : ActionsViewModel() { val conversationId = args.conversationId @@ -60,11 +55,6 @@ class DebugConversationViewModel @AssistedInject constructor( loadConversationDetails() } - @AssistedFactory - interface Factory { - fun create(args: DebugConversationScreenNavArgs): DebugConversationViewModel - } - private fun loadConversationDetails() = viewModelScope.launch { conversationDetails(conversationId) .collect { result -> 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/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/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/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/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/HomeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt index bf50dab6bc3..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 @@ -38,17 +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( +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/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/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/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/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/conversations/attachment/MessageAttachmentsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentsViewModel.kt index e1d300aef87..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 @@ -41,10 +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 dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow @@ -54,9 +50,8 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel(assistedFactory = MessageAttachmentsViewModel.Factory::class) -class MessageAttachmentsViewModel @AssistedInject constructor( - @Assisted conversationNavArgs: ConversationNavArgs, +class MessageAttachmentsViewModel( + conversationNavArgs: ConversationNavArgs, private val assetImporter: MessageAttachmentAssetImporter, private val observeAttachments: ObserveAttachmentDraftsUseCase, private val addAttachment: AddAttachmentDraftUseCase, @@ -277,11 +272,6 @@ class MessageAttachmentsViewModel @AssistedInject constructor( } } } - - @AssistedFactory - interface Factory { - fun create(args: ConversationNavArgs): MessageAttachmentsViewModel - } } sealed interface FailedAttachmentDialogState { 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/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/preview/ImagesPreviewViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModel.kt index fb8ecaed0ca..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 @@ -22,16 +22,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -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.launch -@HiltViewModel(assistedFactory = ImagesPreviewViewModel.Factory::class) -class ImagesPreviewViewModel @AssistedInject constructor( - @Assisted private val navArgs: ImagesPreviewNavArgs, +class ImagesPreviewViewModel( + private val navArgs: ImagesPreviewNavArgs, private val assetImporter: ImagesPreviewAssetImporter ) : ViewModel() { @@ -47,11 +42,6 @@ class ImagesPreviewViewModel @AssistedInject constructor( handleAssets() } - @AssistedFactory - interface Factory { - fun create(args: ImagesPreviewNavArgs): ImagesPreviewViewModel - } - fun onSelected(index: Int) { viewState = viewState.copy(selectedIndex = index) } 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 ed894a37c2a..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 @@ -32,15 +32,10 @@ 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = MessageDraftViewModel.Factory::class) -class MessageDraftViewModel @AssistedInject constructor( - @Assisted private val conversationNavArgs: ConversationNavArgs, +class MessageDraftViewModel( + private val conversationNavArgs: ConversationNavArgs, private val getMessageDraft: GetMessageDraftUseCase, private val getQuotedMessage: GetQuoteMessageForConversationUseCase, private val saveMessageDraft: SaveMessageDraftUseCase, @@ -55,11 +50,6 @@ class MessageDraftViewModel @AssistedInject constructor( loadMessageDraft() } - @AssistedFactory - interface Factory { - fun create(conversationNavArgs: ConversationNavArgs): MessageDraftViewModel - } - fun clearDraft() { viewModelScope.launch { if (state.value.quotedMessageId != null) { 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 7bc06e56b68..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 @@ -27,18 +27,13 @@ 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.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.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = ConversationMigrationViewModel.Factory::class) -class ConversationMigrationViewModel @AssistedInject constructor( - @Assisted private val conversationNavArgs: ConversationNavArgs, +class ConversationMigrationViewModel( + private val conversationNavArgs: ConversationNavArgs, private val observeConversationDetails: ObserveConversationDetailsUseCase ) : ViewModel() { @@ -68,9 +63,4 @@ class ConversationMigrationViewModel @AssistedInject constructor( } } } - - @AssistedFactory - interface Factory { - fun create(args: ConversationNavArgs): ConversationMigrationViewModel - } } 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 81e98d9dc64..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 @@ -73,10 +73,6 @@ 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -85,9 +81,8 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel(assistedFactory = SendMessageViewModel.Factory::class) -class SendMessageViewModel @AssistedInject constructor( - @Assisted private val conversationNavArgs: ConversationNavArgs, +class SendMessageViewModel( + private val conversationNavArgs: ConversationNavArgs, private val sendAssetMessage: ScheduleNewAssetMessageUseCase, private val sendTextMessage: SendTextMessageUseCase, private val sendMultipartMessage: SendMultipartMessageUseCase, @@ -545,9 +540,4 @@ class SendMessageViewModel @AssistedInject constructor( private companion object { const val MAX_LIMIT_MESSAGE_SEND = 20 } - - @AssistedFactory - interface Factory { - fun create(args: ConversationNavArgs): SendMessageViewModel - } } 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 0ce3219eda0..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 @@ -32,17 +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( +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/gallery/MediaGalleryViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModel.kt index 247e78d00d0..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 @@ -40,10 +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.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.asSharedFlow import kotlinx.coroutines.flow.filterIsInstance @@ -53,9 +49,8 @@ import kotlinx.coroutines.withContext import okio.Path @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel(assistedFactory = MediaGalleryViewModel.Factory::class) -class MediaGalleryViewModel @AssistedInject constructor( - @Assisted private val mediaGalleryNavArgs: MediaGalleryNavArgs, +class MediaGalleryViewModel( + private val mediaGalleryNavArgs: MediaGalleryNavArgs, private val getConversationDetails: ObserveConversationDetailsUseCase, private val dispatchers: DispatcherProvider, private val getImageData: GetMessageAssetUseCase, @@ -83,11 +78,6 @@ class MediaGalleryViewModel @AssistedInject constructor( setupImageAsset() } - @AssistedFactory - interface Factory { - fun create(args: MediaGalleryNavArgs): MediaGalleryViewModel - } - private fun setupImageAsset() = viewModelScope.launch { if (cellAssetId == null) { mediaGalleryViewState = mediaGalleryViewState.copy( 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/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/about/dependencies/DependenciesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/about/dependencies/DependenciesViewModel.kt index 226e102cf37..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 @@ -22,13 +22,10 @@ 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.collections.immutable.toImmutableMap import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class DependenciesViewModel @Inject constructor( +class DependenciesViewModel( private val dependenciesInfoProvider: DependenciesInfoProvider ) : ViewModel() { 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 df301e703d9..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 @@ -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 LicensesViewModel @Inject constructor( +class LicensesViewModel( private val licensesProvider: LicensesProvider ) : ViewModel() { 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/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/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/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/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/verifyEmail/VerifyEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/account/email/verifyEmail/VerifyEmailViewModel.kt index 2a1f26e3475..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 @@ -23,16 +23,11 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.kalium.logic.feature.user.UpdateEmailUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = VerifyEmailViewModel.Factory::class) -class VerifyEmailViewModel @AssistedInject constructor( +class VerifyEmailViewModel( private val updateEmail: UpdateEmailUseCase, - @Assisted private val verifyEmailNavArgs: VerifyEmailNavArgs + private val verifyEmailNavArgs: VerifyEmailNavArgs ) : ViewModel() { var state: VerifyEmailState by mutableStateOf(VerifyEmailState()) @@ -40,11 +35,6 @@ class VerifyEmailViewModel @AssistedInject constructor( val newEmail: String = verifyEmailNavArgs.newEmail - @AssistedFactory - interface Factory { - fun create(args: VerifyEmailNavArgs): VerifyEmailViewModel - } - fun onResendVerificationEmailClicked() { newEmail.let { state = state.copy(isResendEmailEnabled = false) 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/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/appsettings/networkSettings/NetworkSettingsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsViewModel.kt index c22c9607b73..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 @@ -29,13 +29,9 @@ 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 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, 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 179df2963f2..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 @@ -49,18 +49,15 @@ 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, 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/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/whatsnew/WhatsNewViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModel.kt index 8b1c9a0d4db..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 @@ -24,14 +24,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.prof18.rssparser.RssParser 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( +class WhatsNewViewModel( private val releaseNotesFeedUrlProvider: ReleaseNotesFeedUrlProvider ) : ViewModel() { private val rssParser = RssParser() 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/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/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/newauthentication/login/NewLoginViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt index 3e98ce1fa19..88b88463f8a 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 @@ -28,8 +28,6 @@ import androidx.lifecycle.viewModelScope 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 @@ -59,20 +57,14 @@ 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.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 import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Named @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel(assistedFactory = NewLoginViewModel.Factory::class) class NewLoginViewModel( private val loginNavArgs: LoginNavArgs, private val validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase, @@ -88,40 +80,6 @@ class NewLoginViewModel( private val recoverableLogoutExceptionDetector: NewLoginRecoverableLogoutExceptionDetector, ) : ActionsViewModel() { - @AssistedInject - constructor( - @Assisted loginNavArgs: LoginNavArgs, - validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase, - @KaliumCoreLogic coreLogic: CoreLogic, - savedInputStore: LoginSavedInputStore, - addAuthenticatedUser: AddAuthenticatedUserUseCase, - clientScopeProviderFactory: ClientScopeProvider.Factory, - userDataStoreProvider: UserDataStoreProvider, - dispatchers: DispatcherProvider, - defaultServerConfig: ServerConfig.Links, - @Named("ssoCodeConfig") defaultSSOCodeConfig: String, - @DefaultWebSocketEnabledByDefault defaultWebSocketEnabledByDefault: Boolean, - recoverableLogoutExceptionDetector: NewLoginRecoverableLogoutExceptionDetector, - ) : this( - loginNavArgs, - validateEmailOrSSOCode, - coreLogic, - savedInputStore, - clientScopeProviderFactory, - userDataStoreProvider, - LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider), - LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic, defaultWebSocketEnabledByDefault), - dispatchers, - defaultServerConfig, - defaultSSOCodeConfig, - recoverableLogoutExceptionDetector - ) - - @AssistedFactory - interface Factory { - fun create(args: LoginNavArgs): NewLoginViewModel - } - private val preFilledUserIdentifier: PreFilledUserIdentifierType = loginNavArgs.userHandle ?: PreFilledUserIdentifierType.None private var pendingNomadServiceUrl: String? = loginNavArgs.ssoCodeAutoLogin?.nomadServiceUrl private var pendingCookieLabel: String? = loginNavArgs.ssoCodeAutoLogin?.cookieLabel 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 9a38ae82665..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 @@ -27,8 +27,6 @@ import androidx.lifecycle.viewModelScope 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 @@ -44,22 +42,17 @@ 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = CreateAccountVerificationCodeViewModel.Factory::class) -class CreateAccountVerificationCodeViewModel @AssistedInject constructor( - @Assisted val createAccountNavArgs: CreateAccountDataNavArgs, - @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 serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig ?: defaultServerConfig @@ -78,11 +71,6 @@ class CreateAccountVerificationCodeViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: CreateAccountDataNavArgs): CreateAccountVerificationCodeViewModel - } - fun resendCode() { codeState = codeState.copy(loading = true) viewModelScope.launch { 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 2e3ea37fb8a..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 @@ -25,7 +25,6 @@ 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 @@ -35,23 +34,18 @@ 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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds -@HiltViewModel(assistedFactory = CreateAccountDataDetailViewModel.Factory::class) -class CreateAccountDataDetailViewModel @AssistedInject constructor( - @Assisted val createAccountNavArgs: CreateAccountDataNavArgs, +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() { @@ -85,11 +79,6 @@ class CreateAccountDataDetailViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: CreateAccountDataNavArgs): CreateAccountDataDetailViewModel - } - private fun onEmailContinue() { detailsState = detailsState.copy(loading = true, continueEnabled = false) viewModelScope.launch { 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 d1b441ef9f5..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 @@ -21,15 +21,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.datastore.GlobalDataStore import com.wire.kalium.logic.configuration.server.ServerConfig -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = CreateAccountSelectorViewModel.Factory::class) -class CreateAccountSelectorViewModel @AssistedInject constructor( - @Assisted navArgs: CreateAccountSelectorNavArgs, +class CreateAccountSelectorViewModel( + navArgs: CreateAccountSelectorNavArgs, private val globalDataStore: GlobalDataStore, defaultServerConfig: ServerConfig.Links ) : ViewModel() { @@ -40,9 +35,4 @@ class CreateAccountSelectorViewModel @AssistedInject constructor( fun onPageLoaded() = viewModelScope.launch { globalDataStore.setAnonymousRegistrationEnabled(false) } - - @AssistedFactory - interface Factory { - fun create(args: CreateAccountSelectorNavArgs): CreateAccountSelectorViewModel - } } 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 b75fc299d35..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 @@ -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 AboutThisAppViewModel @Inject constructor( +class AboutThisAppViewModel( private val aboutThisAppInfoProvider: AboutThisAppInfoProvider ) : ViewModel() { 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 bdb791f316c..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 @@ -51,18 +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.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.distinctUntilChanged import kotlinx.coroutines.launch @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel(assistedFactory = DeviceDetailsViewModel.Factory::class) -class DeviceDetailsViewModel @AssistedInject constructor( - @Assisted private val deviceDetailsNavArgs: DeviceDetailsNavArgs, +class DeviceDetailsViewModel( + private val deviceDetailsNavArgs: DeviceDetailsNavArgs, @CurrentAccount private val currentUserId: UserId, private val deleteClient: DeleteClientUseCase, @@ -96,11 +91,6 @@ class DeviceDetailsViewModel @AssistedInject constructor( getIsE2EIEnabled() } - @AssistedFactory - interface Factory { - fun create(args: DeviceDetailsNavArgs): DeviceDetailsViewModel - } - private fun getIsE2EIEnabled() { viewModelScope.launch { isE2EIEnabledUseCase().let { 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/e2ei/E2eiCertificateDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt index dc71ffb5983..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 @@ -22,15 +22,10 @@ import androidx.lifecycle.viewModelScope import com.wire.android.util.fileDateTime import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.util.DateTimeUtil -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = E2eiCertificateDetailsViewModel.Factory::class) -class E2eiCertificateDetailsViewModel @AssistedInject constructor( - @Assisted private val navArgs: E2eiCertificateDetailsScreenNavArgs, +class E2eiCertificateDetailsViewModel( + private val navArgs: E2eiCertificateDetailsScreenNavArgs, private val getSelfUser: GetSelfUserUseCase, ) : ViewModel() { @@ -40,11 +35,6 @@ class E2eiCertificateDetailsViewModel @AssistedInject constructor( getSelfUserHandle() } - @AssistedFactory - interface Factory { - fun create(args: E2eiCertificateDetailsScreenNavArgs): E2eiCertificateDetailsViewModel - } - private fun getSelfUserHandle() { viewModelScope.launch { selfUserHandle = getSelfUser()?.handle 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 b32be531aa6..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 @@ -36,18 +36,15 @@ 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 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, 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 f574a80afc7..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 @@ -51,10 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow @@ -66,9 +62,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel(assistedFactory = OtherUserProfileScreenViewModel.Factory::class) -class OtherUserProfileScreenViewModel @AssistedInject constructor( - @Assisted private val otherUserProfileNavArgs: OtherUserProfileNavArgs, +class OtherUserProfileScreenViewModel( + private val otherUserProfileNavArgs: OtherUserProfileNavArgs, private val dispatchers: DispatcherProvider, private val observeUserInfo: ObserveUserInfoUseCase, private val userTypeMapper: UserTypeMapper, @@ -104,11 +99,6 @@ class OtherUserProfileScreenViewModel @AssistedInject constructor( getE2EIStatus() } - @AssistedFactory - interface Factory { - fun create(args: OtherUserProfileNavArgs): OtherUserProfileScreenViewModel - } - private fun getIfConversationExist() { viewModelScope.launch { val isOneToOneConversationCreated = isOneToOneConversationCreated(userId) 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 ac0a292662d..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 @@ -27,15 +27,10 @@ import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.user.SelfServerConfigUseCase -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = SelfQRCodeViewModel.Factory::class) -class SelfQRCodeViewModel @AssistedInject constructor( - @Assisted private val selfQrCodeNavArgs: SelfQrCodeNavArgs, +class SelfQRCodeViewModel( + private val selfQrCodeNavArgs: SelfQrCodeNavArgs, @CurrentAccount private val selfUserId: UserId, private val selfServerLinks: SelfServerConfigUseCase, private val qrAssetRepository: SelfQRCodeAssetRepository, @@ -56,11 +51,6 @@ class SelfQRCodeViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: SelfQrCodeNavArgs): SelfQRCodeViewModel - } - suspend fun shareQRAsset(qrCodeImage: SelfQRCodeImage): String = qrAssetRepository.saveQRCode(qrCodeImage) 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/core/ui-common/build.gradle.kts b/core/ui-common/build.gradle.kts index 50762a6c69c..f43bdc1b395 100644 --- a/core/ui-common/build.gradle.kts +++ b/core/ui-common/build.gradle.kts @@ -14,6 +14,8 @@ android { } dependencies { + implementation(project(":core:di")) + implementation("com.wire.kalium:kalium-logic") implementation(libs.androidx.core) implementation(libs.androidx.appcompat) 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/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 dca5bd7c023..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 @@ -53,10 +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.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.MutableSharedFlow @@ -76,10 +72,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @Suppress("TooManyFunctions", "LongParameterList") -@HiltViewModel(assistedFactory = CellViewModel.Factory::class) -class CellViewModel @AssistedInject constructor( - @Assisted private val navArgs: CellFilesNavArgs, - @Assisted private val searchNavArgs: SearchNavArgs?, +class CellViewModel( + private val navArgs: CellFilesNavArgs, + private val searchNavArgs: SearchNavArgs?, private val getCellFilesPaged: GetPaginatedFilesFlowUseCase, private val deleteCellAsset: DeleteCellAssetUseCase, private val restoreNodeFromRecycleBinUseCase: RestoreNodeFromRecycleBinUseCase, @@ -462,11 +457,6 @@ class CellViewModel @AssistedInject constructor( ) ) } - - @AssistedFactory - interface Factory { - fun create(navArgs: CellFilesNavArgs, searchNavArgs: SearchNavArgs?): CellViewModel - } } sealed interface CellViewIntent { 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 760bc49d821..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 @@ -31,17 +31,12 @@ 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.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.map import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = CreateFileViewModel.Factory::class) -class CreateFileViewModel @AssistedInject constructor( - @Assisted private val navArgs: CreateFileScreenNavArgs, +class CreateFileViewModel( + private val navArgs: CreateFileScreenNavArgs, private val createPresentationFileUseCase: CreatePresentationFileUseCase, private val createDocumentFileUseCase: CreateDocumentFileUseCase, private val createSpreadsheetFileUseCase: CreateSpreadsheetFileUseCase @@ -66,11 +61,6 @@ class CreateFileViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: CreateFileScreenNavArgs): CreateFileViewModel - } - internal fun createFile(fileName: String) = viewModelScope.launch { viewState = viewState.copy(loading = true) 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 5bf71a1e705..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 @@ -29,17 +29,12 @@ 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.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.map import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = CreateFolderViewModel.Factory::class) -class CreateFolderViewModel @AssistedInject constructor( - @Assisted private val navArgs: CreateFolderScreenNavArgs, +class CreateFolderViewModel( + private val navArgs: CreateFolderScreenNavArgs, private val createFolderUseCase: CreateFolderUseCase, ) : ActionsViewModel() { @@ -60,11 +55,6 @@ class CreateFolderViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: CreateFolderScreenNavArgs): CreateFolderViewModel - } - internal fun createFolder(folderName: String) = viewModelScope.launch { viewState = viewState.copy(loading = true) 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 b883a5edf99..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 @@ -25,18 +25,13 @@ 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.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.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = MoveToFolderViewModel.Factory::class) -class MoveToFolderViewModel @AssistedInject constructor( - @Assisted private val navArgs: MoveToFolderNavArgs, +class MoveToFolderViewModel( + private val navArgs: MoveToFolderNavArgs, private val getFoldersUseCase: GetFoldersUseCase, private val moveNodeUseCase: MoveNodeUseCase ) : ActionsViewModel() { @@ -63,11 +58,6 @@ class MoveToFolderViewModel @AssistedInject constructor( loadFolders() } - @AssistedFactory - interface Factory { - fun create(args: MoveToFolderNavArgs): MoveToFolderViewModel - } - fun loadFolders() { viewModelScope.launch { getFoldersUseCase(currentPath) 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 84ab1e8624d..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 @@ -28,18 +28,13 @@ 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.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.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = PublicLinkViewModel.Factory::class) -class PublicLinkViewModel @AssistedInject constructor( - @Assisted private val navArgs: PublicLinkNavArgs, +class PublicLinkViewModel( + private val navArgs: PublicLinkNavArgs, private val createPublicLink: CreatePublicLinkUseCase, private val getPublicLinkUseCase: GetPublicLinkUseCase, private val deletePublicLinkUseCase: DeletePublicLinkUseCase, @@ -225,11 +220,6 @@ class PublicLinkViewModel @AssistedInject constructor( PublicLinkError.Remove -> onConfirmRemoval(true) } } - - @AssistedFactory - interface Factory { - fun create(navArgs: PublicLinkNavArgs): PublicLinkViewModel - } } internal data class PublicLinkViewState( 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 70f1a4d11f2..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 @@ -27,10 +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.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.asStateFlow import kotlinx.coroutines.flow.update @@ -43,9 +39,8 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime -@HiltViewModel(assistedFactory = PublicLinkExpirationScreenViewModel.Factory::class) -internal class PublicLinkExpirationScreenViewModel @AssistedInject constructor( - @Assisted private val navArgs: PublicLinkExpirationScreenNavArgs, +internal class PublicLinkExpirationScreenViewModel( + private val navArgs: PublicLinkExpirationScreenNavArgs, val setExpiration: SetPublicLinkExpirationUseCase, ) : ActionsViewModel() { @@ -194,11 +189,6 @@ internal class PublicLinkExpirationScreenViewModel @AssistedInject constructor( private fun updateState(block: PublicLinkExpirationScreenViewState.() -> PublicLinkExpirationScreenViewState) { _state.update(block) } - - @AssistedFactory - interface Factory { - fun create(navArgs: PublicLinkExpirationScreenNavArgs): PublicLinkExpirationScreenViewModel - } } internal sealed class ExpirationError( 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 c48a4e0dce4..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 @@ -30,19 +30,14 @@ 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.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.asStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = PublicLinkPasswordScreenViewModel.Factory::class) -internal class PublicLinkPasswordScreenViewModel @AssistedInject constructor( - @Assisted private val navArgs: PublicLinkPasswordNavArgs, +internal class PublicLinkPasswordScreenViewModel( + private val navArgs: PublicLinkPasswordNavArgs, private val generateRandomPassword: RandomPassword, private val createPassword: CreatePublicLinkPasswordUseCase, private val updatePassword: UpdatePublicLinkPasswordUseCase, @@ -180,11 +175,6 @@ internal class PublicLinkPasswordScreenViewModel @AssistedInject constructor( _state.update(block) } - @AssistedFactory - interface Factory { - fun create(navArgs: PublicLinkPasswordNavArgs): PublicLinkPasswordScreenViewModel - } - private companion object { const val TYPING_DEBOUNCE_TIME = 200L } 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 575551a8df0..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 @@ -31,10 +31,6 @@ 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.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.collectLatest @@ -42,9 +38,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds -@HiltViewModel(assistedFactory = RenameNodeViewModel.Factory::class) -class RenameNodeViewModel @AssistedInject constructor( - @Assisted private val navArgs: RenameNodeNavArgs, +class RenameNodeViewModel( + private val navArgs: RenameNodeNavArgs, private val renameNodeUseCase: RenameNodeUseCase, ) : ActionsViewModel() { @@ -71,11 +66,6 @@ class RenameNodeViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: RenameNodeNavArgs): RenameNodeViewModel - } - fun renameNode(newName: String) { viewState = viewState.copy(loading = true) viewModelScope.launch { 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 429a1d6aad3..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 @@ -49,10 +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.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.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -70,9 +66,8 @@ import kotlinx.coroutines.launch private const val SEARCH_DEBOUNCE_MILLIS = 200L @Suppress("TooManyFunctions") -@HiltViewModel(assistedFactory = SearchScreenViewModel.Factory::class) -class SearchScreenViewModel @AssistedInject constructor( - @Assisted private val navArgs: SearchNavArgs, +class SearchScreenViewModel( + private val navArgs: SearchNavArgs, private val getAllTagsUseCase: GetAllTagsUseCase, private val getCellFilesPaged: GetPaginatedFilesFlowUseCase, private val getOwners: GetOwnersUseCase, @@ -372,11 +367,6 @@ class SearchScreenViewModel @AssistedInject constructor( current.copy(sortingCriteria = criteria) } } - - @AssistedFactory - interface Factory { - fun create(navArgs: SearchNavArgs): SearchScreenViewModel - } } private fun CellConversation.toFilterConversationUi() = FilterConversationUi( 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 df0086be17b..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 @@ -27,10 +27,6 @@ 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.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.asStateFlow import kotlinx.coroutines.flow.collectLatest @@ -38,9 +34,8 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -@HiltViewModel(assistedFactory = AddRemoveTagsViewModel.Factory::class) -class AddRemoveTagsViewModel @AssistedInject constructor( - @Assisted private val navArgs: AddRemoveTagsNavArgs, +class AddRemoveTagsViewModel( + private val navArgs: AddRemoveTagsNavArgs, private val getAllTagsUseCase: GetAllTagsUseCase, private val updateNodeTagsUseCase: UpdateNodeTagsUseCase, private val removeNodeTagsUseCase: RemoveNodeTagsUseCase, @@ -65,11 +60,6 @@ class AddRemoveTagsViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(args: AddRemoveTagsNavArgs): AddRemoveTagsViewModel - } - fun isValidTag(): Boolean = with(tagsTextState) { disallowedChars.none { it in text } && text.length in ALLOWED_LENGTH } 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 41d920b3bfc..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 @@ -39,10 +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.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -52,9 +48,8 @@ import java.time.LocalDate import java.time.ZoneId import java.time.format.DateTimeFormatter -@HiltViewModel(assistedFactory = VersionHistoryViewModel.Factory::class) -class VersionHistoryViewModel @AssistedInject constructor( - @Assisted private val navArgs: VersionHistoryNavArgs, +class VersionHistoryViewModel( + private val navArgs: VersionHistoryNavArgs, private val getNodeVersionsUseCase: GetNodeVersionsUseCase, private val fileSizeFormatter: FileSizeFormatter, private val restoreNodeVersionUseCase: RestoreNodeVersionUseCase, @@ -270,9 +265,4 @@ class VersionHistoryViewModel @AssistedInject constructor( const val DELAY_100_MS = 100L const val DELAY_500_MS = 500L } - - @AssistedFactory - interface Factory { - fun create(navArgs: VersionHistoryNavArgs): VersionHistoryViewModel - } } diff --git a/features/meetings/build.gradle.kts b/features/meetings/build.gradle.kts index b6f34267b1b..9d79d361c9d 100644 --- a/features/meetings/build.gradle.kts +++ b/features/meetings/build.gradle.kts @@ -13,6 +13,7 @@ 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) 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 1b899633344..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 @@ -23,25 +23,20 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource -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 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 @@ -49,7 +44,6 @@ import com.wire.android.ui.common.rowitem.EmptyListArrowFooter import com.wire.android.ui.common.rowitem.EmptyListContent import com.wire.android.ui.common.rowitem.LoadingListContent import com.wire.android.ui.theme.WireTheme -import com.wire.android.util.dispatchers.DefaultDispatcherProvider @Composable fun MeetingList( @@ -59,7 +53,7 @@ fun MeetingList( openMeetingOptions: (meetingId: String) -> Unit = {}, meetingListViewModel: MeetingListViewModel = when { LocalInspectionMode.current -> MeetingListViewModelPreview(CurrentTimeProvider.Preview, type) - else -> metroViewModel(key = "meeting_list_${type.name}") { + else -> metroViewModel(key = "meeting_list_${type.name}") { meetingListViewModelFactory.create(type = type) } }, @@ -185,31 +179,3 @@ fun MeetingListNextPreview() = WireTheme { fun MeetingListPastPreview() = WireTheme { MeetingList(type = MeetingsTabItem.PAST) } - -private class MeetingListMetroFactories { - val meetingListViewModelFactory = MeetingListViewModelFactory(DefaultDispatcherProvider()) -} - -@Composable -private inline fun metroViewModel( - viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { - "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" - }, - key: String? = null, - crossinline create: MeetingListMetroFactories.() -> VM, -): VM { - val factories = remember { MeetingListMetroFactories() } - val factory = remember(factories) { - viewModelFactory { - initializer { - factories.create() - } - } - } - return viewModel( - modelClass = VM::class, - viewModelStoreOwner = viewModelStoreOwner, - key = key, - factory = factory, - ) -} 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/MeetingOptionsModalSheetLayout.kt b/features/meetings/src/main/java/com/wire/android/feature/meetings/ui/options/MeetingOptionsModalSheetLayout.kt index ef5f14e2ca8..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 @@ -20,21 +20,16 @@ package com.wire.android.feature.meetings.ui.options import android.annotation.SuppressLint import androidx.compose.material3.Icon import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment 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.lifecycle.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -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 +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 @@ -59,7 +54,9 @@ fun MeetingOptionsModalSheetLayout( sheetState: WireModalSheetState, viewModel: MeetingOptionsMenuViewModel = when { LocalInspectionMode.current -> MeetingOptionsMenuViewModelPreview(CurrentTimeProvider.Preview) - else -> metroViewModel { meetingOptionsMenuViewModelFactory.create() } + else -> metroViewModel { + meetingOptionsMenuViewModelFactory.create() + } } ) { val deletedMeetingOptionsClosedMessage = stringResource(R.string.deleted_meeting_options_closed) @@ -184,32 +181,6 @@ private fun MutableList.addIf(condition: Boolean, element: E) { if (condition) add(element) } -private class MeetingOptionsMetroFactories { - val meetingOptionsMenuViewModelFactory = MeetingOptionsMenuViewModelFactory() -} - -@Composable -private inline fun metroViewModel( - viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { - "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" - }, - crossinline create: MeetingOptionsMetroFactories.() -> VM, -): VM { - val factories = remember { MeetingOptionsMetroFactories() } - val factory = remember(factories) { - viewModelFactory { - initializer { - factories.create() - } - } - } - return viewModel( - modelClass = VM::class, - viewModelStoreOwner = viewModelStoreOwner, - factory = factory, - ) -} - @PreviewMultipleThemes @Composable fun PreviewMessageOptionsModalSheetLayout() = WireTheme { From 5ca58113889dcf752bba33d3eb6adbeafd586bc1 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 11:27:31 +0200 Subject: [PATCH 25/46] refactor: reduce Hilt surface for Metro migration --- ...izeRegistrationAnalyticsMetadataUseCase.kt | 2 +- .../RegistrationAnalyticsManagerUseCase.kt | 2 +- .../config/NomadProfilesFeatureConfig.kt | 2 +- .../kotlin/com/wire/android/di/AppModule.kt | 8 + .../wire/android/di/AssistedViewModelExt.kt | 54 ---- .../android/di/accountScoped/AppsModule.kt | 47 ---- .../di/accountScoped/AuthenticationModule.kt | 29 +- .../android/di/accountScoped/CallsModule.kt | 213 +------------- .../android/di/accountScoped/CellsModule.kt | 4 - .../di/accountScoped/ChannelsModule.kt | 35 +-- .../android/di/accountScoped/ClientModule.kt | 93 +------ .../di/accountScoped/ConnectionModule.kt | 43 +-- .../android/di/accountScoped/DebugModule.kt | 98 +------ .../android/di/accountScoped/SearchModule.kt | 44 --- .../di/accountScoped/ServicesModule.kt | 76 ----- .../android/di/accountScoped/TeamModule.kt | 35 +-- .../android/di/accountScoped/UserModule.kt | 259 +----------------- .../wire/android/di/metro/WireMetroGraph.kt | 89 ++++++ .../android/feature/AccountSwitchUseCase.kt | 3 +- .../android/feature/DisableAppLockUseCase.kt | 3 +- .../feature/ObserveAppLockConfigUseCase.kt | 3 +- ...dStartPersistentWebSocketServiceUseCase.kt | 3 +- ...rtPersistentWebsocketIfNecessaryUseCase.kt | 3 +- .../com/wire/android/media/CallRinger.kt | 3 +- .../media/audiomessage/AudioFocusHelper.kt | 3 +- .../ConversationAudioMessagePlayer.kt | 5 +- .../audiomessage/RecordAudioMessagePlayer.kt | 3 +- .../BroadcastReceiverDependencies.kt | 73 +++++ .../EndOngoingCallReceiver.kt | 30 +- .../IncomingCallActionReceiver.kt | 37 +-- .../broadcastreceivers/NomadLogoutReceiver.kt | 39 +-- .../NotificationReplyReceiver.kt | 24 +- .../PlayPauseAudioMessageReceiver.kt | 18 +- .../StopAudioMessageReceiver.kt | 18 +- .../com/wire/android/services/CallService.kt | 23 +- .../services/PersistentWebSocketService.kt | 24 +- .../services/PlayingAudioMessageService.kt | 18 +- .../wire/android/ui/CallFeedbackViewModel.kt | 5 +- .../ui/CallFeedbackViewModelFactory.kt | 2 + .../com/wire/android/ui/WireActivity.kt | 9 +- .../wire/android/ui/WireActivityViewModel.kt | 5 +- .../ui/WireActivityViewModelFactory.kt | 2 + .../com/wire/android/ui/WireTestActivity.kt | 2 - .../wire/android/ui/calling/CallActivity.kt | 5 +- .../ui/calling/CallActivityViewModel.kt | 5 +- .../calling/CallActivityViewModelFactory.kt | 2 + .../android/ui/debug/StartServiceReceiver.kt | 23 +- .../home/conversations/MessageSharedState.kt | 2 +- ...bserveConversationMembersByTypesUseCase.kt | 2 +- .../ObserveReactionsForMessageUseCase.kt | 2 +- .../ObserveReceiptsForMessageUseCase.kt | 2 +- ...GetAssetMessagesFromConversationUseCase.kt | 2 +- ...etConversationMessagesFromSearchUseCase.kt | 2 +- .../GetConversationsFromSearchUseCase.kt | 2 +- .../GetMessagesForConversationUseCase.kt | 2 +- .../GetQuoteMessageForConversationUseCase.kt | 2 +- .../usecase/GetUsersForMessageUseCase.kt | 2 +- .../usecase/HandleUriAssetUseCase.kt | 2 +- ...ageAssetMessagesFromConversationUseCase.kt | 2 +- .../ObserveMessageForConversationUseCase.kt | 2 +- ...serveQuoteMessageForConversationUseCase.kt | 2 +- ...ObserveUsersTypingInConversationUseCase.kt | 2 +- .../location/GeocoderHelper.kt | 2 +- .../GenerateAudioFileWithEffectsUseCase.kt | 3 +- .../code/NewLoginVerificationCodeScreen.kt | 2 +- .../worker/AssetUploadObserverWorker.kt | 8 +- .../worker/DeleteConversationLocallyWorker.kt | 10 +- .../worker/NotificationFetchWorker.kt | 11 +- .../worker/PersistentWebsocketCheckWorker.kt | 11 +- .../com/wire/android/media/PingRinger.kt | 3 +- .../NotificationChannelsManager.kt | 3 +- .../wire/android/util/FileSizeFormatter.kt | 2 +- .../ui/AndroidCellFileExternalActions.kt | 3 +- .../feature/cells/ui/CellFileActionsMenu.kt | 2 +- .../cells/ui/CellFileLocalPathCache.kt | 2 +- .../cells/ui/OpenFileDownloadController.kt | 2 +- .../feature/cells/ui/edit/OnlineEditor.kt | 2 +- .../android/feature/cells/util/FileHelper.kt | 3 +- .../feature/cells/util/FileNameResolver.kt | 2 +- .../wire/android/sync/InitialSyncWorker.kt | 10 +- 80 files changed, 348 insertions(+), 1289 deletions(-) delete mode 100644 app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt delete mode 100644 app/src/main/kotlin/com/wire/android/di/accountScoped/ServicesModule.kt create mode 100644 app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/BroadcastReceiverDependencies.kt 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/di/AppModule.kt b/app/src/main/kotlin/com/wire/android/di/AppModule.kt index 932a3d00e94..0f425b3331a 100644 --- a/app/src/main/kotlin/com/wire/android/di/AppModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/AppModule.kt @@ -26,12 +26,14 @@ import android.media.AudioManager import android.media.MediaPlayer import androidx.core.app.NotificationManagerCompat import com.wire.android.BuildConfig +import com.wire.android.config.NomadProfilesFeatureConfig 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.GeocoderHelper import com.wire.android.ui.home.messagecomposer.location.LocationPickerParameters import com.wire.android.util.GetMediaMetadataUseCase import com.wire.android.util.GetMediaMetadataUseCaseImpl @@ -104,6 +106,9 @@ object AppModule { @Provides fun provideGeocoder(appContext: Context): Geocoder = Geocoder(appContext) + @Provides + fun provideGeocoderHelper(geocoder: Geocoder): GeocoderHelper = GeocoderHelper(geocoder) + @Provides fun provideLocationPickerParameters(): LocationPickerParameters = LocationPickerParameters() @@ -114,6 +119,9 @@ object AppModule { @Provides fun provideAnonymousAnalyticsManager(): AnonymousAnalyticsManager = AnonymousAnalyticsManagerImpl + @Provides + fun provideNomadProfilesFeatureConfig(): NomadProfilesFeatureConfig = NomadProfilesFeatureConfig() + @Provides fun provideAudioManager(@ApplicationContext context: Context): AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager diff --git a/app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt b/app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt deleted file mode 100644 index 63c165df87a..00000000000 --- a/app/src/main/kotlin/com/wire/android/di/AssistedViewModelExt.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Wire - * Copyright (C) 2026 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.android.di - -import androidx.activity.ComponentActivity -import androidx.activity.viewModels -import androidx.compose.runtime.Composable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelStoreOwner -import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner -import androidx.hilt.navigation.compose.hiltViewModel -import dagger.hilt.android.lifecycle.withCreationCallback - -inline fun ComponentActivity.assistedViewModels( - crossinline create: (VMF) -> VM -) = viewModels(extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback { factory -> create(factory) } -}) - -@Composable -inline fun wireViewModel( - viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { - "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" - }, - key: String? = null, -): VM = hiltViewModel(viewModelStoreOwner = viewModelStoreOwner, key = key) - -@Composable -inline fun wireViewModel( - viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { - "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" - }, - key: String? = null, - noinline creationCallback: (VMF) -> VM -): VM = hiltViewModel( - viewModelStoreOwner = viewModelStoreOwner, - key = key, - creationCallback = creationCallback -) 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/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..42d9e352664 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 @@ -19,7 +19,6 @@ 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 @@ -204,9 +203,6 @@ class CellsModule { @Provides fun provideGetOwnersUseCase(cellsScope: CellsScope): GetOwnersUseCase = cellsScope.getOwnersUseCase - @Provides - fun provideFileNameResolver(): FileNameResolver = FileNameResolver() - @Provides fun provideGetCellNodeUseCase(cellsScope: CellsScope): GetCellFileUseCase = cellsScope.getCellFileUseCase 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/DebugModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt index c60703ddad7..35ae70d8969 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt @@ -17,100 +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.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 -} +// Debug 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/WireMetroGraph.kt b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt index 8f382b3cb94..d9d57c82142 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -23,10 +23,12 @@ 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.analytics.FinalizeRegistrationAnalyticsMetadataUseCase import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase +import com.wire.android.config.NomadProfilesFeatureConfig import com.wire.android.config.ServerConfigProvider import com.wire.android.datastore.GlobalDataStore import com.wire.android.datastore.UserDataStore @@ -76,10 +78,19 @@ 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.NotificationChannelsManager +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.common.banner.SecurityClassificationViewModelFactory import com.wire.android.ui.common.bottomsheet.conversation.ConversationOptionsMenuViewModelFactory import com.wire.android.ui.authentication.create.code.CreateAccountCodeViewModelFactory @@ -758,11 +769,17 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset 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 dispatcherProvider: DispatcherProvider + val persistentWebSocketServiceDependencies: PersistentWebSocketService.Dependencies + val callServiceDependencies: CallService.Dependencies + val playingAudioMessageServiceDependencies: PlayingAudioMessageService.Dependencies @get:CurrentAccount val currentAccount: UserId @@ -890,6 +907,17 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideWireNotificationManager(entryPoint: WireMetroHiltEntryPoint): WireNotificationManager = entryPoint.wireNotificationManager() + @Provides + fun provideNotificationManagerCompat(@ApplicationContext context: Context): NotificationManagerCompat = + NotificationManagerCompat.from(context) + + @Provides + fun provideNotificationChannelsManager( + @ApplicationContext context: Context, + notificationManagerCompat: NotificationManagerCompat, + ): NotificationChannelsManager = + NotificationChannelsManager(context, notificationManagerCompat) + @Provides fun provideLocationPickerHelperFlavor(entryPoint: WireMetroHiltEntryPoint): LocationPickerHelperFlavor = entryPoint.locationPickerHelperFlavor() @@ -906,6 +934,27 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideAnonymousAnalyticsManager(): AnonymousAnalyticsManager = AnonymousAnalyticsManagerImpl + @Provides + fun provideAnonymousAnalyticsManagerLazy( + anonymousAnalyticsManager: AnonymousAnalyticsManager, + ): dagger.Lazy = + object : dagger.Lazy { + override fun get(): AnonymousAnalyticsManager = anonymousAnalyticsManager + } + + @Provides + fun provideNomadProfilesFeatureConfig(): NomadProfilesFeatureConfig = + NomadProfilesFeatureConfig() + + @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 @@ -3142,6 +3191,46 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideCallNotificationManager(entryPoint: WireMetroHiltEntryPoint): CallNotificationManager = entryPoint.callNotificationManager() + @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, 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..879dfc81faf 100644 --- a/app/src/main/kotlin/com/wire/android/feature/DisableAppLockUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/DisableAppLockUseCase.kt @@ -21,10 +21,11 @@ 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/ConversationAudioMessagePlayer.kt b/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt index b74e10133b7..8be10c8c76f 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 @@ -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..7793c265875 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 @@ -34,10 +34,11 @@ 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/notification/broadcastreceivers/BroadcastReceiverDependencies.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/BroadcastReceiverDependencies.kt new file mode 100644 index 00000000000..a5b5efb94c9 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/BroadcastReceiverDependencies.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.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.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 dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope + +@EntryPoint +@InstallIn(SingletonComponent::class) +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() = EntryPointAccessors.fromApplication( + applicationContext, + BroadcastReceiverDependencies::class.java + ) 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/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 index d01c2b72d95..ff64bb058c1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModelFactory.kt @@ -23,7 +23,9 @@ 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, 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 ae673c16444..d63ec398988 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 @@ -166,11 +165,15 @@ class WireActivity : BaseActivity() { createWireMetroGraph(this) } - private val viewModel: WireActivityViewModel by viewModels() + private val viewModel: WireActivityViewModel by metroActivityViewModel { + wireActivityViewModelFactory.create() + } private val featureFlagNotificationViewModel: FeatureFlagNotificationViewModel by metroActivityViewModel { featureFlagNotificationViewModelFactory.create() } - private val callFeedbackViewModel: CallFeedbackViewModel by viewModels() + private val callFeedbackViewModel: CallFeedbackViewModel by metroActivityViewModel { + callFeedbackViewModelFactory.create() + } private val commonTopAppBarViewModel: CommonTopAppBarViewModel by metroActivityViewModel { commonTopAppBarViewModelFactory.create( 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 f434fc36253..ab16f07106b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -89,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 @@ -109,14 +108,12 @@ 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, diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt index 85782054d52..e1bb7d981c2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModelFactory.kt @@ -44,7 +44,9 @@ 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, 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/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt index 5f15c3f4cff..3f86614ebaf 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 @@ -87,7 +86,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) { 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 index 8307b0a71dc..3efcd51536a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModelFactory.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivityViewModelFactory.kt @@ -21,7 +21,9 @@ 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, 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/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/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/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/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/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/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/newauthentication/code/NewLoginVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/code/NewLoginVerificationCodeScreen.kt index d5345da35ab..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 wireViewModel() + loginEmailViewModel: LoginEmailViewModel, // provided in MainNavHost to reuse from NewLoginPasswordScreen ) { clearAutofillTree() LoginStateNavigationAndDialogs(loginEmailViewModel, navigator) 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/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/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/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..64842294ef5 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 @@ -20,7 +20,7 @@ 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 dev.zacsweers.metro.Inject class FileSizeFormatter @Inject constructor( @ApplicationContext private val context: Context 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 index 0549d2a5fb6..04e88bc9193 100644 --- 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 @@ -19,9 +19,10 @@ package com.wire.android.feature.cells.ui import com.wire.android.feature.cells.util.FileHelper import okio.Path.Companion.toPath +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject -class AndroidCellFileExternalActions @Inject constructor( +class AndroidCellFileExternalActions @Inject @MetroInject constructor( private val fileHelper: FileHelper, ) : CellFileExternalActions { 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/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/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/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/util/FileHelper.kt b/features/cells/src/main/java/com/wire/android/feature/cells/util/FileHelper.kt index 8d6dfa14980..3b3c6460db9 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 @@ -31,9 +31,10 @@ import okio.Path import java.io.File import java.io.FileOutputStream import java.io.OutputStream +import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject -class FileHelper @Inject constructor( +class FileHelper @Inject @MetroInject 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/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) { From dc5ce0ff83a41c05dac98e5a7c4be5be63c593b4 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 12:29:33 +0200 Subject: [PATCH 26/46] refactor: reduce remaining Hilt surface for Metro --- .../config/NomadProfilesFeatureConfig.kt | 3 +- .../kotlin/com/wire/android/di/AppModule.kt | 8 - .../com/wire/android/di/ImageLoadingModule.kt | 40 -- .../com/wire/android/di/LogWriterModule.kt | 2 +- .../android/di/ManagedConfigurationsModule.kt | 9 +- .../android/di/accountScoped/BackupModule.kt | 62 +-- .../android/di/accountScoped/CellsModule.kt | 240 +---------- .../di/accountScoped/ConversationModule.kt | 404 +----------------- .../android/di/accountScoped/MessageModule.kt | 244 +---------- .../wire/android/di/metro/WireMetroGraph.kt | 145 ++++++- .../DynamicReceiversManager.kt | 8 +- .../com/wire/android/ui/AppLockActivity.kt | 13 +- .../com/wire/android/ui/WireActivity.kt | 36 +- .../login/LoginSavedInputStoreModule.kt | 14 +- .../wire/android/ui/calling/CallActivity.kt | 23 +- .../ui/calling/StartingCallActivity.kt | 2 - .../ui/calling/ongoing/OngoingCallActivity.kt | 16 +- .../MessageAttachmentAssetImporter.kt | 3 +- .../MessageAttachmentFileGateway.kt | 3 +- .../attachment/MessageAttachmentModule.kt | 16 +- .../TempWritableAttachmentUriProvider.kt | 16 +- .../preview/ImagesPreviewAssetImporter.kt | 3 +- .../media/preview/ImagesPreviewModule.kt | 13 +- .../messages/ConversationAssetFileGateway.kt | 14 +- .../location/GeocoderHelper.kt | 3 +- .../recordaudio/AudioMediaRecorder.kt | 5 +- .../recordaudio/RecordAudioFileGateway.kt | 17 +- .../dependencies/DependenciesInfoProvider.kt | 17 +- .../about/licenses/LicensesProvider.kt | 20 +- .../NetworkSettingsDefaultsProvider.kt | 20 +- .../home/settings/backup/BackupFileGateway.kt | 14 +- .../whatsnew/ReleaseNotesFeedUrlProvider.kt | 19 +- .../about/AboutThisAppInfoProvider.kt | 6 +- .../ui/settings/about/AboutThisAppModule.kt | 12 +- .../ui/sharing/ImportMediaAssetImporter.kt | 3 +- .../ui/sharing/ImportMediaSharingModule.kt | 13 +- .../avatarpicker/AvatarImageGateway.kt | 17 +- .../qr/AndroidSelfQRCodeAssetRepository.kt | 6 +- .../ui/userprofile/qr/SelfQRCodeModule.kt | 15 +- .../services/WireFirebaseMessagingService.kt | 22 +- 40 files changed, 225 insertions(+), 1321 deletions(-) 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 d77e8a38bfa..726cf5e17c6 100644 --- a/app/src/main/kotlin/com/wire/android/config/NomadProfilesFeatureConfig.kt +++ b/app/src/main/kotlin/com/wire/android/config/NomadProfilesFeatureConfig.kt @@ -20,7 +20,8 @@ package com.wire.android.config import com.wire.android.BuildConfig import dev.zacsweers.metro.Inject +import javax.inject.Inject as HiltInject -class NomadProfilesFeatureConfig @Inject constructor() { +class NomadProfilesFeatureConfig @Inject @HiltInject constructor() { fun isEnabled(): Boolean = BuildConfig.NOMAD_PROFILES_ENABLED } 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 0f425b3331a..932a3d00e94 100644 --- a/app/src/main/kotlin/com/wire/android/di/AppModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/AppModule.kt @@ -26,14 +26,12 @@ import android.media.AudioManager import android.media.MediaPlayer import androidx.core.app.NotificationManagerCompat import com.wire.android.BuildConfig -import com.wire.android.config.NomadProfilesFeatureConfig 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.GeocoderHelper import com.wire.android.ui.home.messagecomposer.location.LocationPickerParameters import com.wire.android.util.GetMediaMetadataUseCase import com.wire.android.util.GetMediaMetadataUseCaseImpl @@ -106,9 +104,6 @@ object AppModule { @Provides fun provideGeocoder(appContext: Context): Geocoder = Geocoder(appContext) - @Provides - fun provideGeocoderHelper(geocoder: Geocoder): GeocoderHelper = GeocoderHelper(geocoder) - @Provides fun provideLocationPickerParameters(): LocationPickerParameters = LocationPickerParameters() @@ -119,9 +114,6 @@ object AppModule { @Provides fun provideAnonymousAnalyticsManager(): AnonymousAnalyticsManager = AnonymousAnalyticsManagerImpl - @Provides - fun provideNomadProfilesFeatureConfig(): NomadProfilesFeatureConfig = NomadProfilesFeatureConfig() - @Provides fun provideAudioManager(@ApplicationContext context: Context): AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager 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/LogWriterModule.kt b/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt index f90c9093484..fc41c82ffce 100644 --- a/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt @@ -20,8 +20,8 @@ package com.wire.android.di import android.content.Context import com.wire.android.BuildConfig +import com.wire.android.util.logging.LogFileWriter 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 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..c4213b3cd80 100644 --- a/app/src/main/kotlin/com/wire/android/di/ManagedConfigurationsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/ManagedConfigurationsModule.kt @@ -42,10 +42,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) class ManagedConfigurationsModule { - @Provides - @Singleton - fun provideServerConfigProvider(): ServerConfigProvider = ServerConfigProvider() - @Provides @Singleton fun provideAndroidUserContextProvider(): AndroidUserContextProvider = @@ -77,14 +73,15 @@ class ManagedConfigurationsModule { @Provides fun provideCurrentServerConfig( - managedConfigurationsManager: ManagedConfigurationsManager + managedConfigurationsManager: ManagedConfigurationsManager, + serverConfigProvider: ServerConfigProvider, ): 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) + serverConfigProvider.getDefaultServerConfig(null) } } 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/CellsModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/CellsModule.kt index 42d9e352664..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,242 +17,4 @@ */ package com.wire.android.di.accountScoped -import com.wire.android.di.CurrentAccount -import com.wire.android.di.KaliumCoreLogic -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 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/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/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/metro/WireMetroGraph.kt b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt index d9d57c82142..e88a7132c6f 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -26,6 +26,7 @@ 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.analytics.FinalizeRegistrationAnalyticsMetadataUseCase import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase import com.wire.android.config.NomadProfilesFeatureConfig @@ -44,9 +45,12 @@ 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.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 @@ -79,6 +83,7 @@ import com.wire.android.model.ImageAssetViewModelGraph import com.wire.android.notification.WireNotificationManager import com.wire.android.notification.CallNotificationManager 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 @@ -91,6 +96,7 @@ 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 @@ -295,12 +301,15 @@ import com.wire.android.util.FileSizeFormatter 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.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.time.ISOFormatter import com.wire.android.util.time.TimeZoneProvider @@ -308,6 +317,7 @@ 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 @@ -632,6 +642,20 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.runBlocking +data class WireApplicationDependencies( + @KaliumCoreLogic val coreLogic: dagger.Lazy, + val logFileWriter: dagger.Lazy, + val syncLifecycleManager: dagger.Lazy, + val wireWorkerFactory: dagger.Lazy, + val globalObserversManager: dagger.Lazy, + val globalDataStore: dagger.Lazy, + val userDataStoreProvider: dagger.Lazy, + @ApplicationScope val globalAppScope: CoroutineScope, + val currentScreenManager: CurrentScreenManager, + val analyticsManager: dagger.Lazy, + val workManager: WorkManager, +) + abstract class WireMetroScope private constructor() @DependencyGraph(WireMetroScope::class) @@ -776,7 +800,22 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset 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 proximitySensorManager: ProximitySensorManager + val servicesManager: ServicesManager + val callNotificationManager: CallNotificationManager + val dispatcherProvider: DispatcherProvider + + @get:KaliumCoreLogic + val coreLogic: CoreLogic + val networkUtil: NetworkUtil + val wireApplicationDependencies: WireApplicationDependencies val persistentWebSocketServiceDependencies: PersistentWebSocketService.Dependencies val callServiceDependencies: CallService.Dependencies val playingAudioMessageServiceDependencies: PlayingAudioMessageService.Dependencies @@ -942,18 +981,45 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset override fun get(): AnonymousAnalyticsManager = anonymousAnalyticsManager } + @Provides + fun provideWireApplicationDependencies( + entryPoint: WireMetroHiltEntryPoint, + @KaliumCoreLogic coreLogic: dagger.Lazy, + globalDataStore: dagger.Lazy, + analyticsManager: dagger.Lazy, + workManager: WorkManager, + ): WireApplicationDependencies = + WireApplicationDependencies( + coreLogic = coreLogic, + logFileWriter = object : dagger.Lazy { + override fun get(): LogFileWriter = entryPoint.logFileWriter() + }, + syncLifecycleManager = object : dagger.Lazy { + override fun get(): SyncLifecycleManager = entryPoint.syncLifecycleManager() + }, + wireWorkerFactory = object : dagger.Lazy { + override fun get(): WireWorkerFactory = entryPoint.wireWorkerFactory() + }, + globalObserversManager = object : dagger.Lazy { + override fun get(): GlobalObserversManager = entryPoint.globalObserversManager() + }, + globalDataStore = globalDataStore, + userDataStoreProvider = object : dagger.Lazy { + override fun get(): UserDataStoreProvider = entryPoint.userDataStoreProvider() + }, + globalAppScope = entryPoint.applicationScope(), + currentScreenManager = entryPoint.currentScreenManager(), + analyticsManager = analyticsManager, + workManager = workManager, + ) + @Provides fun provideNomadProfilesFeatureConfig(): NomadProfilesFeatureConfig = NomadProfilesFeatureConfig() @Provides - fun provideLoginTypeSelector( - @KaliumCoreLogic coreLogic: dagger.Lazy, - ): LoginTypeSelector = - LoginTypeSelector( - coreLogic = coreLogic, - useNewLoginForDefaultBackend = BuildConfig.USE_NEW_LOGIN_FOR_DEFAULT_BACKEND, - ) + fun provideLoginTypeSelector(entryPoint: WireMetroHiltEntryPoint): LoginTypeSelector = + entryPoint.loginTypeSelector() @Provides fun provideCurrentSessionFlowUseCase(@KaliumCoreLogic coreLogic: CoreLogic): CurrentSessionFlowUseCase = @@ -1361,8 +1427,52 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset ScreenStateObserver(context) @Provides - fun provideCurrentScreenManager(screenStateObserver: ScreenStateObserver): CurrentScreenManager = - CurrentScreenManager(screenStateObserver) + fun provideCurrentScreenManager(entryPoint: WireMetroHiltEntryPoint): CurrentScreenManager = + entryPoint.currentScreenManager() + + @Provides + fun provideSwitchAccountObserver(entryPoint: WireMetroHiltEntryPoint): SwitchAccountObserver = + entryPoint.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 + fun provideProximitySensorManager(entryPoint: WireMetroHiltEntryPoint): ProximitySensorManager = + entryPoint.proximitySensorManager() @Provides fun provideGenerateAudioFileWithEffectsUseCase(dispatchers: DispatcherProvider): GenerateAudioFileWithEffectsUseCase = @@ -3297,6 +3407,14 @@ interface WireMetroHiltEntryPoint { fun automatedLoginManager(): AutomatedLoginManager + fun currentScreenManager(): CurrentScreenManager + + fun switchAccountObserver(): SwitchAccountObserver + + fun loginTypeSelector(): LoginTypeSelector + + fun proximitySensorManager(): ProximitySensorManager + fun managedConfigurationsManager(): ManagedConfigurationsManager fun lockCodeTimeManager(): LockCodeTimeManager @@ -3311,6 +3429,15 @@ interface WireMetroHiltEntryPoint { fun logFileWriter(): LogFileWriter + fun syncLifecycleManager(): SyncLifecycleManager + + fun wireWorkerFactory(): WireWorkerFactory + + fun globalObserversManager(): GlobalObserversManager + + @ApplicationScope + fun applicationScope(): CoroutineScope + fun accountSwitchUseCase(): AccountSwitchUseCase fun servicesManager(): ServicesManager 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/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/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index d63ec398988..ebfed5f931a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -77,10 +77,8 @@ import com.wire.android.config.LocalCustomUiConfigurationProvider import com.wire.android.datastore.UserDataStore import com.wire.android.di.metro.WireMetroGraph import com.wire.android.di.metro.createWireMetroGraph -import com.wire.android.emm.ManagedConfigurationsManager 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 @@ -90,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 @@ -106,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 @@ -119,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 @@ -136,34 +128,20 @@ 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 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() @@ -634,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 { 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 index b08f58ba084..2cef8b8a028 100644 --- 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 @@ -18,16 +18,4 @@ package com.wire.android.ui.authentication.login -import androidx.lifecycle.SavedStateHandle -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent - -@Module -@InstallIn(ViewModelComponent::class) -object LoginSavedInputStoreModule { - @Provides - fun provideLoginSavedInputStore(savedStateHandle: SavedStateHandle): LoginSavedInputStore = - SavedStateLoginSavedInputStore(savedStateHandle) -} +// Login saved input store bindings are provided by the Metro graph. 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 3f86614ebaf..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 @@ -41,36 +41,32 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.wire.android.appLogger +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 - - private val metroGraph by lazy(LazyThreadSafetyMode.NONE) { + 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: CommonTopAppBarViewModel by metroActivityViewModel { commonTopAppBarViewModelFactory.create( @@ -114,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( 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/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/home/conversations/attachment/MessageAttachmentAssetImporter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/attachment/MessageAttachmentAssetImporter.kt index 99820282289..8cbfd3f7ed2 100644 --- 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 @@ -20,13 +20,12 @@ 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 -import javax.inject.Inject interface MessageAttachmentAssetImporter { suspend fun importAsset(uri: String): ImportedMediaAsset? } -class MessageAttachmentAssetImporterImpl @Inject constructor( +class MessageAttachmentAssetImporterImpl( private val handleUriAsset: HandleUriAssetUseCase, ) : MessageAttachmentAssetImporter { 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 index 508bf3accd8..0aeb45f4c57 100644 --- 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 @@ -26,7 +26,6 @@ import com.wire.kalium.logic.util.fileExtension import okio.Path import okio.Path.Companion.toPath import java.io.File -import javax.inject.Inject interface MessageAttachmentFileGateway { fun exists(localFilePath: String): Boolean @@ -34,7 +33,7 @@ interface MessageAttachmentFileGateway { fun audioMetadata(dataPath: Path, mimeType: String, wavesMask: List?): AssetContent.AssetMetadata.Audio } -class MessageAttachmentFileGatewayImpl @Inject constructor( +class MessageAttachmentFileGatewayImpl( private val fileManager: FileManager, ) : MessageAttachmentFileGateway { 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 index 2242e19e870..3281f608f02 100644 --- 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 @@ -17,18 +17,4 @@ */ package com.wire.android.ui.home.conversations.attachment -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent - -@Module -@InstallIn(ViewModelComponent::class) -interface MessageAttachmentModule { - - @Binds - fun bindMessageAttachmentAssetImporter(impl: MessageAttachmentAssetImporterImpl): MessageAttachmentAssetImporter - - @Binds - fun bindMessageAttachmentFileGateway(impl: MessageAttachmentFileGatewayImpl): MessageAttachmentFileGateway -} +// Message attachment bindings are provided by the Metro graph. 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 index 591fee587f7..4ec521e7579 100644 --- 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 @@ -20,18 +20,13 @@ package com.wire.android.ui.home.conversations.composer import com.wire.android.util.FileManager import com.wire.kalium.logic.data.asset.KaliumFileSystem -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import javax.inject.Inject interface TempWritableAttachmentUriProvider { suspend fun getTempWritableVideoUri(): String suspend fun getTempWritableImageUri(): String } -class AndroidTempWritableAttachmentUriProvider @Inject constructor( +class AndroidTempWritableAttachmentUriProvider( private val fileManager: FileManager, private val kaliumFileSystem: KaliumFileSystem, ) : TempWritableAttachmentUriProvider { @@ -41,12 +36,3 @@ class AndroidTempWritableAttachmentUriProvider @Inject constructor( override suspend fun getTempWritableImageUri(): String = fileManager.getTempWritableImageUri(kaliumFileSystem.rootCachePath).toString() } - -@Module -@InstallIn(ViewModelComponent::class) -interface TempWritableAttachmentUriProviderModule { - @Binds - fun bindTempWritableAttachmentUriProvider( - provider: AndroidTempWritableAttachmentUriProvider - ): TempWritableAttachmentUriProvider -} 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 index 52bc40dd41e..c8c6b249194 100644 --- 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 @@ -22,13 +22,12 @@ 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 -import javax.inject.Inject interface ImagesPreviewAssetImporter { suspend fun importAsset(uri: String): ImportedMediaAsset? } -class ImagesPreviewAssetImporterImpl @Inject constructor( +class ImagesPreviewAssetImporterImpl( private val handleUriAsset: HandleUriAssetUseCase, private val dispatchers: DispatcherProvider, ) : ImagesPreviewAssetImporter { 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 index af79c82c9ce..32488a66755 100644 --- 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 @@ -17,15 +17,4 @@ */ package com.wire.android.ui.home.conversations.media.preview -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent - -@Module -@InstallIn(ViewModelComponent::class) -interface ImagesPreviewModule { - - @Binds - fun bindImagesPreviewAssetImporter(impl: ImagesPreviewAssetImporterImpl): ImagesPreviewAssetImporter -} +// Images preview bindings are provided by the Metro graph. 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 index 425d809d35d..86b0d665b1a 100644 --- 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 @@ -19,12 +19,7 @@ package com.wire.android.ui.home.conversations.messages import com.wire.android.util.FileManager -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent import okio.Path -import javax.inject.Inject interface ConversationAssetFileGateway { fun openWithExternalApp(assetDataPath: Path, assetName: String?, onError: () -> Unit) @@ -32,7 +27,7 @@ interface ConversationAssetFileGateway { fun shareWithExternalApp(assetDataPath: Path, assetName: String?) } -class AndroidConversationAssetFileGateway @Inject constructor( +class AndroidConversationAssetFileGateway( private val fileManager: FileManager, ) : ConversationAssetFileGateway { @@ -52,10 +47,3 @@ class AndroidConversationAssetFileGateway @Inject constructor( fileManager.shareWithExternalApp(assetDataPath, assetName) {} } } - -@Module -@InstallIn(ViewModelComponent::class) -interface ConversationAssetFileGatewayModule { - @Binds - fun bindConversationAssetFileGateway(gateway: AndroidConversationAssetFileGateway): ConversationAssetFileGateway -} 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 e51b1d5b004..c099843a3f9 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 @@ -20,8 +20,9 @@ package com.wire.android.ui.home.messagecomposer.location import android.location.Geocoder import android.location.Location import dev.zacsweers.metro.Inject +import javax.inject.Inject as HiltInject -class GeocoderHelper @Inject constructor(private val geocoder: Geocoder) { +class GeocoderHelper @Inject @HiltInject constructor(private val geocoder: Geocoder) { @Suppress("TooGenericExceptionCaught") fun getGeoLocatedAddress(location: Location): GeoLocatedAddress = 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/RecordAudioFileGateway.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/recordaudio/RecordAudioFileGateway.kt index 32eed487dfb..a2d9949af46 100644 --- 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 @@ -22,14 +22,8 @@ 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 dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.qualifiers.ApplicationContext import okio.Path import java.io.File -import javax.inject.Inject interface RecordAudioFileGateway { suspend fun generateAudioFileWithEffects( @@ -41,8 +35,8 @@ interface RecordAudioFileGateway { fun contentUri(audioFile: File): Uri } -class AndroidRecordAudioFileGateway @Inject constructor( - @ApplicationContext private val context: Context, +class AndroidRecordAudioFileGateway( + private val context: Context, private val generateAudioFileWithEffects: GenerateAudioFileWithEffectsUseCase, ) : RecordAudioFileGateway { @@ -66,10 +60,3 @@ class AndroidRecordAudioFileGateway @Inject constructor( override fun contentUri(audioFile: File): Uri = context.fromNioPathToContentUri(nioPath = audioFile.toPath()) } - -@Module -@InstallIn(ViewModelComponent::class) -interface RecordAudioFileGatewayModule { - @Binds - fun bindRecordAudioFileGateway(gateway: AndroidRecordAudioFileGateway): RecordAudioFileGateway -} 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 index 18f496b0613..3dc8cc0c975 100644 --- 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 @@ -19,26 +19,13 @@ package com.wire.android.ui.home.settings.about.dependencies import android.content.Context import com.wire.android.util.getDependenciesVersion -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject interface DependenciesInfoProvider { suspend fun dependenciesVersion(): Map } -class AndroidDependenciesInfoProvider @Inject constructor( - @ApplicationContext private val context: Context +class AndroidDependenciesInfoProvider( + private val context: Context ) : DependenciesInfoProvider { override suspend fun dependenciesVersion(): Map = context.getDependenciesVersion() } - -@Module -@InstallIn(ViewModelComponent::class) -interface DependenciesInfoProviderModule { - @Binds - fun bindDependenciesInfoProvider(provider: AndroidDependenciesInfoProvider): 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 index 37af538fc86..c2d3cc33354 100644 --- 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 @@ -21,21 +21,15 @@ import android.content.Context import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.entity.Library import com.mikepenz.aboutlibraries.util.withContext -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import javax.inject.Inject interface LicensesProvider { suspend fun getLibraries(): List } -class AndroidLicensesProvider @Inject constructor( - @ApplicationContext private val context: Context +class AndroidLicensesProvider( + private val context: Context ) : LicensesProvider { override suspend fun getLibraries(): List = withContext(Dispatchers.IO) { @@ -46,13 +40,3 @@ class AndroidLicensesProvider @Inject constructor( .distinctBy { it.uniqueId } } } - -@Module -@InstallIn(ViewModelComponent::class) -interface LicensesProviderModule { - - @Binds - fun bindLicensesProvider( - provider: AndroidLicensesProvider - ): LicensesProvider -} 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 index e564b41728d..8ba62a0e197 100644 --- 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 @@ -20,31 +20,15 @@ package com.wire.android.ui.home.settings.appsettings.networkSettings import android.content.Context import com.wire.android.util.isWebsocketEnabledByDefault -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject interface NetworkSettingsDefaultsProvider { val isWebSocketEnabledByDefault: Boolean } -class AndroidNetworkSettingsDefaultsProvider @Inject constructor( - @ApplicationContext private val context: Context +class AndroidNetworkSettingsDefaultsProvider( + private val context: Context ) : NetworkSettingsDefaultsProvider { override val isWebSocketEnabledByDefault: Boolean get() = isWebsocketEnabledByDefault(context) } - -@Module -@InstallIn(ViewModelComponent::class) -interface NetworkSettingsDefaultsProviderModule { - - @Binds - fun bindNetworkSettingsDefaultsProvider( - provider: AndroidNetworkSettingsDefaultsProvider - ): NetworkSettingsDefaultsProvider -} 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 index b29fb7d8f9d..0e5007cdb7e 100644 --- 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 @@ -22,13 +22,8 @@ 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 dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent import kotlinx.coroutines.withContext import okio.Path -import javax.inject.Inject interface BackupFileGateway { suspend fun shareBackup(path: Path, assetName: String?) @@ -37,7 +32,7 @@ interface BackupFileGateway { suspend fun deleteImportedBackup(path: Path) } -class AndroidBackupFileGateway @Inject constructor( +class AndroidBackupFileGateway( private val fileManager: FileManager, private val kaliumFileSystem: KaliumFileSystem, private val dispatcher: DispatcherProvider, @@ -65,10 +60,3 @@ class AndroidBackupFileGateway @Inject constructor( } } } - -@Module -@InstallIn(ViewModelComponent::class) -interface BackupFileGatewayModule { - @Binds - fun bindBackupFileGateway(gateway: AndroidBackupFileGateway): BackupFileGateway -} 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 index f491f97f74b..00c0bef54db 100644 --- 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 @@ -19,29 +19,14 @@ package com.wire.android.ui.home.whatsnew import android.content.Context import com.wire.android.R -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject interface ReleaseNotesFeedUrlProvider { val feedUrl: String } -class AndroidReleaseNotesFeedUrlProvider @Inject constructor( - @ApplicationContext private val context: Context +class AndroidReleaseNotesFeedUrlProvider( + private val context: Context ) : ReleaseNotesFeedUrlProvider { override val feedUrl: String get() = context.resources.getString(R.string.url_android_release_notes_feed) } - -@Module -@InstallIn(ViewModelComponent::class) -interface ReleaseNotesFeedUrlProviderModule { - @Binds - fun bindReleaseNotesFeedUrlProvider( - provider: AndroidReleaseNotesFeedUrlProvider - ): ReleaseNotesFeedUrlProvider -} 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 index a6da3c69986..d818a033662 100644 --- 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 @@ -20,16 +20,14 @@ package com.wire.android.ui.settings.about import android.content.Context import com.wire.android.util.AppNameUtil import com.wire.android.util.getGitBuildId -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject interface AboutThisAppInfoProvider { val appName: String fun gitBuildId(): String } -class AndroidAboutThisAppInfoProvider @Inject constructor( - @ApplicationContext private val context: Context +class AndroidAboutThisAppInfoProvider( + private val context: Context ) : AboutThisAppInfoProvider { override val appName: String get() = AppNameUtil.createAppName() diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppModule.kt b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppModule.kt index 8648bfec26a..e773e737a14 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppModule.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppModule.kt @@ -17,14 +17,4 @@ */ package com.wire.android.ui.settings.about -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -interface AboutThisAppModule { - @Binds - fun bindAboutThisAppInfoProvider(provider: AndroidAboutThisAppInfoProvider): AboutThisAppInfoProvider -} +// About-this-app bindings are provided by the Metro graph. 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 index 96be9eaef09..3384dacff2f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAssetImporter.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAssetImporter.kt @@ -22,13 +22,12 @@ 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 -import javax.inject.Inject interface ImportMediaAssetImporter { suspend fun importAsset(uri: String): ImportedMediaAsset? } -class ImportMediaAssetImporterImpl @Inject constructor( +class ImportMediaAssetImporterImpl( private val handleUriAsset: HandleUriAssetUseCase, private val dispatchers: DispatcherProvider, ) : ImportMediaAssetImporter { 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 index b3b088da816..d30907eced2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingModule.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaSharingModule.kt @@ -17,15 +17,4 @@ */ package com.wire.android.ui.sharing -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent - -@Module -@InstallIn(ViewModelComponent::class) -interface ImportMediaSharingModule { - - @Binds - fun bindImportMediaAssetImporter(impl: ImportMediaAssetImporterImpl): ImportMediaAssetImporter -} +// 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 index ecd78991ce4..9872c0c32bf 100644 --- 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 @@ -25,13 +25,7 @@ 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 dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.qualifiers.ApplicationContext import okio.Path -import javax.inject.Inject interface AvatarImageGateway { fun getWritableAvatarUri(avatarPath: Path): String @@ -40,10 +34,10 @@ interface AvatarImageGateway { suspend fun getAvatarImageSize(avatarUri: String): Long } -class AndroidAvatarImageGateway @Inject constructor( +class AndroidAvatarImageGateway( private val avatarImageManager: AvatarImageManager, private val dispatchers: DispatcherProvider, - @ApplicationContext private val appContext: Context + private val appContext: Context ) : AvatarImageGateway { override fun getWritableAvatarUri(avatarPath: Path): String = @@ -68,10 +62,3 @@ class AndroidAvatarImageGateway @Inject constructor( override suspend fun getAvatarImageSize(avatarUri: String): Long = avatarUri.toUri().toByteArray(appContext, dispatchers).size.toLong() } - -@Module -@InstallIn(ViewModelComponent::class) -interface AvatarImageGatewayModule { - @Binds - fun bindAvatarImageGateway(gateway: AndroidAvatarImageGateway): AvatarImageGateway -} 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 index 3b77ef5c6da..aafa0d8f0f4 100644 --- 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 @@ -22,14 +22,12 @@ 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 dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.withContext import okio.Path.Companion.toPath import java.io.FileOutputStream -import javax.inject.Inject -class AndroidSelfQRCodeAssetRepository @Inject constructor( - @ApplicationContext private val context: Context, +class AndroidSelfQRCodeAssetRepository( + private val context: Context, private val kaliumFileSystem: KaliumFileSystem, private val dispatchers: DispatcherProvider ) : SelfQRCodeAssetRepository { diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt index 8dbcde60e86..e675247e755 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeModule.kt @@ -17,17 +17,4 @@ */ package com.wire.android.ui.userprofile.qr -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent - -@Module -@InstallIn(ViewModelComponent::class) -interface SelfQRCodeModule { - - @Binds - fun bindSelfQRCodeAssetRepository( - repository: AndroidSelfQRCodeAssetRepository - ): SelfQRCodeAssetRepository -} +// Self QR code bindings are provided by the Metro graph. 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() } From 15dbc7514ad6b8a60e8a5f985cf03142559edf0b Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 12:39:31 +0200 Subject: [PATCH 27/46] fix: stabilize Metro application scope --- .../wire/android/di/metro/WireMetroGraph.kt | 52 +------------------ 1 file changed, 2 insertions(+), 50 deletions(-) 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 index e88a7132c6f..b18023a1917 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -639,23 +639,8 @@ import dev.zacsweers.metro.Named import dev.zacsweers.metro.Provides import dev.zacsweers.metro.createGraphFactory import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.runBlocking -data class WireApplicationDependencies( - @KaliumCoreLogic val coreLogic: dagger.Lazy, - val logFileWriter: dagger.Lazy, - val syncLifecycleManager: dagger.Lazy, - val wireWorkerFactory: dagger.Lazy, - val globalObserversManager: dagger.Lazy, - val globalDataStore: dagger.Lazy, - val userDataStoreProvider: dagger.Lazy, - @ApplicationScope val globalAppScope: CoroutineScope, - val currentScreenManager: CurrentScreenManager, - val analyticsManager: dagger.Lazy, - val workManager: WorkManager, -) - abstract class WireMetroScope private constructor() @DependencyGraph(WireMetroScope::class) @@ -815,7 +800,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset @get:KaliumCoreLogic val coreLogic: CoreLogic val networkUtil: NetworkUtil - val wireApplicationDependencies: WireApplicationDependencies val persistentWebSocketServiceDependencies: PersistentWebSocketService.Dependencies val callServiceDependencies: CallService.Dependencies val playingAudioMessageServiceDependencies: PlayingAudioMessageService.Dependencies @@ -981,38 +965,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset override fun get(): AnonymousAnalyticsManager = anonymousAnalyticsManager } - @Provides - fun provideWireApplicationDependencies( - entryPoint: WireMetroHiltEntryPoint, - @KaliumCoreLogic coreLogic: dagger.Lazy, - globalDataStore: dagger.Lazy, - analyticsManager: dagger.Lazy, - workManager: WorkManager, - ): WireApplicationDependencies = - WireApplicationDependencies( - coreLogic = coreLogic, - logFileWriter = object : dagger.Lazy { - override fun get(): LogFileWriter = entryPoint.logFileWriter() - }, - syncLifecycleManager = object : dagger.Lazy { - override fun get(): SyncLifecycleManager = entryPoint.syncLifecycleManager() - }, - wireWorkerFactory = object : dagger.Lazy { - override fun get(): WireWorkerFactory = entryPoint.wireWorkerFactory() - }, - globalObserversManager = object : dagger.Lazy { - override fun get(): GlobalObserversManager = entryPoint.globalObserversManager() - }, - globalDataStore = globalDataStore, - userDataStoreProvider = object : dagger.Lazy { - override fun get(): UserDataStoreProvider = entryPoint.userDataStoreProvider() - }, - globalAppScope = entryPoint.applicationScope(), - currentScreenManager = entryPoint.currentScreenManager(), - analyticsManager = analyticsManager, - workManager = workManager, - ) - @Provides fun provideNomadProfilesFeatureConfig(): NomadProfilesFeatureConfig = NomadProfilesFeatureConfig() @@ -1382,8 +1334,8 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset @ApplicationScope @Provides - fun provideApplicationCoroutineScope(dispatchers: DispatcherProvider): CoroutineScope = - CoroutineScope(SupervisorJob() + dispatchers.default()) + fun provideApplicationCoroutineScope(entryPoint: WireMetroHiltEntryPoint): CoroutineScope = + entryPoint.applicationScope() @Provides fun provideMusicMediaPlayer(): MediaPlayer = From 8cffc810139121c99e0f4ec2a9e009af83f259ac Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 12:58:45 +0200 Subject: [PATCH 28/46] chore: remove redundant Metro providers --- .../wire/android/di/metro/WireMetroGraph.kt | 303 ------------------ 1 file changed, 303 deletions(-) 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 index b18023a1917..7c1dd63a59e 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -27,9 +27,6 @@ import androidx.core.app.NotificationManagerCompat import androidx.work.WorkManager import com.wire.android.BuildConfig import com.wire.android.GlobalObserversManager -import com.wire.android.analytics.FinalizeRegistrationAnalyticsMetadataUseCase -import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase -import com.wire.android.config.NomadProfilesFeatureConfig import com.wire.android.config.ServerConfigProvider import com.wire.android.datastore.GlobalDataStore import com.wire.android.datastore.UserDataStore @@ -49,18 +46,12 @@ 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.CellFileActionsMenu import com.wire.android.feature.cells.ui.CellFileExternalActions -import com.wire.android.feature.cells.ui.CellFileLocalPathCache -import com.wire.android.feature.cells.ui.OpenFileDownloadController -import com.wire.android.feature.cells.ui.edit.OnlineEditor import com.wire.android.feature.cells.util.FileHelper -import com.wire.android.feature.cells.util.FileNameResolver import com.wire.android.media.audiomessage.AudioFocusHelper import com.wire.android.media.audiomessage.AudioMessageViewModelFactory import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer @@ -140,14 +131,12 @@ import com.wire.android.ui.home.conversations.details.editselfdeletingmessages.E 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.MessageSharedState 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.banner.usecase.ObserveConversationMembersByTypesUseCase 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 @@ -161,8 +150,6 @@ import com.wire.android.ui.home.conversations.details.updateappsaccess.UpdateApp 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.messagedetails.usecase.ObserveReactionsForMessageUseCase -import com.wire.android.ui.home.conversations.messagedetails.usecase.ObserveReceiptsForMessageUseCase 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 @@ -183,17 +170,7 @@ import com.wire.android.ui.home.conversations.search.adddembertoconversation.Add 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.GetAssetMessagesFromConversationUseCase -import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase -import com.wire.android.ui.home.conversations.usecase.GetConversationsFromSearchUseCase -import com.wire.android.ui.home.conversations.usecase.GetMessagesForConversationUseCase -import com.wire.android.ui.home.conversations.usecase.GetQuoteMessageForConversationUseCase -import com.wire.android.ui.home.conversations.usecase.GetUsersForMessageUseCase import com.wire.android.ui.home.conversations.usecase.HandleUriAssetUseCase -import com.wire.android.ui.home.conversations.usecase.ObserveImageAssetMessagesFromConversationUseCase -import com.wire.android.ui.home.conversations.usecase.ObserveMessageForConversationUseCase -import com.wire.android.ui.home.conversations.usecase.ObserveQuoteMessageForConversationUseCase -import com.wire.android.ui.home.conversations.usecase.ObserveUsersTypingInConversationUseCase import com.wire.android.ui.home.conversations.typing.TypingIndicatorViewModelFactory import com.wire.android.ui.home.AppSyncViewModelFactory import com.wire.android.ui.home.conversationslist.ConversationListCallViewModelFactory @@ -934,13 +911,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideNotificationManagerCompat(@ApplicationContext context: Context): NotificationManagerCompat = NotificationManagerCompat.from(context) - @Provides - fun provideNotificationChannelsManager( - @ApplicationContext context: Context, - notificationManagerCompat: NotificationManagerCompat, - ): NotificationChannelsManager = - NotificationChannelsManager(context, notificationManagerCompat) - @Provides fun provideLocationPickerHelperFlavor(entryPoint: WireMetroHiltEntryPoint): LocationPickerHelperFlavor = entryPoint.locationPickerHelperFlavor() @@ -965,10 +935,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset override fun get(): AnonymousAnalyticsManager = anonymousAnalyticsManager } - @Provides - fun provideNomadProfilesFeatureConfig(): NomadProfilesFeatureConfig = - NomadProfilesFeatureConfig() - @Provides fun provideLoginTypeSelector(entryPoint: WireMetroHiltEntryPoint): LoginTypeSelector = entryPoint.loginTypeSelector() @@ -1201,13 +1167,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset override fun get(): SelfServerConfigUseCase = selfServerConfig } - @Provides - fun provideDisableAppLockUseCase( - globalDataStore: GlobalDataStore, - observeIsAppLockEditable: ObserveIsAppLockEditableUseCase, - ): DisableAppLockUseCase = - DisableAppLockUseCase(globalDataStore, observeIsAppLockEditable) - @Provides fun provideDisableAppLockUseCaseLazy(disableAppLockUseCase: DisableAppLockUseCase): dagger.Lazy = object : dagger.Lazy { @@ -1352,10 +1311,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideAudioManager(@ApplicationContext context: Context): AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - @Provides - fun provideAudioFocusHelper(audioManager: AudioManager): AudioFocusHelper = - AudioFocusHelper(audioManager) - @Provides fun provideRecordAudioMessagePlayer( @ApplicationContext context: Context, @@ -1426,10 +1381,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideProximitySensorManager(entryPoint: WireMetroHiltEntryPoint): ProximitySensorManager = entryPoint.proximitySensorManager() - @Provides - fun provideGenerateAudioFileWithEffectsUseCase(dispatchers: DispatcherProvider): GenerateAudioFileWithEffectsUseCase = - GenerateAudioFileWithEffectsUseCase(dispatchers) - @Provides fun provideRecordAudioFileGateway( @ApplicationContext context: Context, @@ -1647,10 +1598,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideMessageResourceProvider(): MessageResourceProvider = MessageResourceProvider() - @Provides - fun provideMessageSharedState(): MessageSharedState = - MessageSharedState() - @Provides fun provideGetMediaMetadataUseCase(): GetMediaMetadataUseCase = GetMediaMetadataUseCaseImpl() @@ -1758,28 +1705,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideAnalyticsConfiguration(): AnalyticsConfiguration = if (BuildConfig.ANALYTICS_ENABLED) AnalyticsConfiguration.Enabled else AnalyticsConfiguration.Disabled - @Provides - fun provideRegistrationAnalyticsManagerUseCase( - globalDataStore: GlobalDataStore, - anonymousAnalyticsManager: AnonymousAnalyticsManager, - ): RegistrationAnalyticsManagerUseCase = - RegistrationAnalyticsManagerUseCase( - globalDataStore = globalDataStore, - anonymousAnalyticsManager = anonymousAnalyticsManager, - ) - - @Provides - fun provideFinalizeRegistrationAnalyticsMetadataUseCase( - globalDataStore: GlobalDataStore, - @CurrentAccount currentAccount: UserId, - @KaliumCoreLogic coreLogic: CoreLogic, - ): FinalizeRegistrationAnalyticsMetadataUseCase = - FinalizeRegistrationAnalyticsMetadataUseCase( - globalDataStore = globalDataStore, - currentAccount = currentAccount, - coreLogic = coreLogic, - ) - @Provides fun provideObserveReadReceiptsEnabledUseCase(userScope: UserScope): ObserveReadReceiptsEnabledUseCase = userScope.observeReadReceiptsEnabled @@ -1868,16 +1793,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideNewLoginRecoverableLogoutExceptionDetector(): NewLoginRecoverableLogoutExceptionDetector = NewLoginRecoverableLogoutExceptionDetector() - @Provides - fun provideObserveAppLockConfigUseCase( - globalDataStore: GlobalDataStore, - @KaliumCoreLogic coreLogic: CoreLogic, - ): ObserveAppLockConfigUseCase = - ObserveAppLockConfigUseCase( - globalDataStore = globalDataStore, - coreLogic = coreLogic, - ) - @Provides fun provideMarkTeamAppLockStatusAsNotifiedUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @@ -2104,16 +2019,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset ): ObserveConversationUnderLegalHoldNotifiedUseCase = conversationScope.observeConversationUnderLegalHoldNotified - @Provides - fun provideObserveConversationMembersByTypesUseCase( - observeConversationMembers: ObserveConversationMembersUseCase, - dispatchers: DispatcherProvider, - ): ObserveConversationMembersByTypesUseCase = - ObserveConversationMembersByTypesUseCase( - observeConversationMembers = observeConversationMembers, - dispatchers = dispatchers, - ) - @Provides fun provideResetMLSConversationUseCase( @KaliumCoreLogic coreLogic: CoreLogic, @@ -2160,16 +2065,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideObserveUserListByIdUseCase(conversationScope: ConversationScope): ObserveUserListByIdUseCase = conversationScope.observeUserListById - @Provides - fun provideObserveUsersTypingInConversationUseCase( - observeUsersTyping: ObserveUsersTypingUseCase, - uiParticipantMapper: UIParticipantMapper, - ): ObserveUsersTypingInConversationUseCase = - ObserveUsersTypingInConversationUseCase( - observeUsersTyping = observeUsersTyping, - uiParticipantMapper = uiParticipantMapper, - ) - @Provides fun provideObserveConversationRoleForUserUseCase( observeConversationMembers: ObserveConversationMembersUseCase, @@ -2546,121 +2441,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideObserveMessageByIdUseCase(messageScope: MessageScope): ObserveMessageByIdUseCase = messageScope.observeMessageById - @Provides - fun provideGetUsersForMessageUseCase( - observeUserListById: ObserveUserListByIdUseCase, - messageMapper: MessageMapper, - ): GetUsersForMessageUseCase = - GetUsersForMessageUseCase(observeUserListById, messageMapper) - - @Provides - fun provideObserveMessageForConversationUseCase( - observeMessageById: ObserveMessageByIdUseCase, - getUsersForMessage: GetUsersForMessageUseCase, - messageMapper: MessageMapper, - dispatchers: DispatcherProvider, - ): ObserveMessageForConversationUseCase = - ObserveMessageForConversationUseCase( - observeMessage = observeMessageById, - getUsersForMessage = getUsersForMessage, - messageMapper = messageMapper, - dispatchers = dispatchers, - ) - - @Provides - fun provideObserveQuoteMessageForConversationUseCase( - observeMessageById: ObserveMessageByIdUseCase, - getUsersForMessage: GetUsersForMessageUseCase, - messageMapper: MessageMapper, - dispatchers: DispatcherProvider, - ): ObserveQuoteMessageForConversationUseCase = - ObserveQuoteMessageForConversationUseCase( - observeMessageById = observeMessageById, - getUsersForMessage = getUsersForMessage, - messageMapper = messageMapper, - dispatchers = dispatchers, - ) - - @Provides - fun provideGetQuoteMessageForConversationUseCase( - getMessageById: GetMessageByIdUseCase, - getUsersForMessage: GetUsersForMessageUseCase, - messageMapper: MessageMapper, - dispatchers: DispatcherProvider, - ): GetQuoteMessageForConversationUseCase = - GetQuoteMessageForConversationUseCase( - getMessageById = getMessageById, - getUsersForMessage = getUsersForMessage, - messageMapper = messageMapper, - dispatchers = dispatchers, - ) - - @Provides - fun provideGetMessagesForConversationUseCase( - getMessages: GetPaginatedFlowOfMessagesByConversationUseCase, - getUsersForMessage: GetUsersForMessageUseCase, - messageMapper: MessageMapper, - dispatchers: DispatcherProvider, - ): GetMessagesForConversationUseCase = - GetMessagesForConversationUseCase( - getMessages = getMessages, - getUsersForMessage = getUsersForMessage, - messageMapper = messageMapper, - dispatchers = dispatchers, - ) - - @Provides - fun provideGetAssetMessagesFromConversationUseCase( - getAssetMessages: GetPaginatedFlowOfAssetMessageByConversationIdUseCase, - getUsersForMessage: GetUsersForMessageUseCase, - messageMapper: MessageMapper, - dispatchers: DispatcherProvider, - ): GetAssetMessagesFromConversationUseCase = - GetAssetMessagesFromConversationUseCase( - getAssetMessages = getAssetMessages, - getUsersForMessage = getUsersForMessage, - messageMapper = messageMapper, - dispatchers = dispatchers, - ) - - @Provides - fun provideObserveImageAssetMessagesFromConversationUseCase( - getAssetMessages: ObservePaginatedAssetImageMessages, - assetMapper: UIAssetMapper, - dispatchers: DispatcherProvider, - timeZoneProvider: TimeZoneProvider, - ): ObserveImageAssetMessagesFromConversationUseCase = - ObserveImageAssetMessagesFromConversationUseCase( - getAssetMessages = getAssetMessages, - assetMapper = assetMapper, - dispatchers = dispatchers, - timeZoneProvider = timeZoneProvider, - ) - - @Provides - fun provideObserveReactionsForMessageUseCase( - observeMessageReactions: ObserveMessageReactionsUseCase, - uiParticipantMapper: UIParticipantMapper, - dispatchers: DispatcherProvider, - ): ObserveReactionsForMessageUseCase = - ObserveReactionsForMessageUseCase( - observeMessageReactions = observeMessageReactions, - uiParticipantMapper = uiParticipantMapper, - dispatchers = dispatchers, - ) - - @Provides - fun provideObserveReceiptsForMessageUseCase( - observeMessageReceipts: ObserveMessageReceiptsUseCase, - uiParticipantMapper: UIParticipantMapper, - dispatchers: DispatcherProvider, - ): ObserveReceiptsForMessageUseCase = - ObserveReceiptsForMessageUseCase( - observeMessageReceipts = observeMessageReceipts, - uiParticipantMapper = uiParticipantMapper, - dispatchers = dispatchers, - ) - @Provides fun provideCellsScope( @KaliumCoreLogic coreLogic: CoreLogic, @@ -2816,18 +2596,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideGetWireCellConfigurationUseCase(cellsScope: CellsScope): GetWireCellConfigurationUseCase = cellsScope.getCellConfig - @Provides - fun provideOnlineEditor(@ApplicationContext context: Context): OnlineEditor = - OnlineEditor(context) - - @Provides - fun provideCellsFileHelper(@ApplicationContext context: Context): FileHelper = - FileHelper(context) - - @Provides - fun provideFileNameResolver(): FileNameResolver = - FileNameResolver() - @Provides fun provideFileSizeFormatter(@ApplicationContext context: Context): FileSizeFormatter = FileSizeFormatter(context) @@ -2836,28 +2604,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideCellFileExternalActions(fileHelper: FileHelper): CellFileExternalActions = AndroidCellFileExternalActions(fileHelper) - @Provides - fun provideCellFileActionsMenu(kaliumConfigs: KaliumConfigs): CellFileActionsMenu = - CellFileActionsMenu(kaliumConfigs) - - @Provides - fun provideCellFileLocalPathCache(): CellFileLocalPathCache = - CellFileLocalPathCache() - - @Provides - fun provideOpenFileDownloadController( - downloadCellFile: DownloadCellFileUseCase, - fileHelper: FileHelper, - fileNameResolver: FileNameResolver, - sharedPathCache: CellFileLocalPathCache, - ): OpenFileDownloadController = - OpenFileDownloadController( - download = downloadCellFile, - fileHelper = fileHelper, - fileNameResolver = fileNameResolver, - sharedPathCache = sharedPathCache, - ) - @Provides fun provideCellAssetRefreshHelper( refreshAsset: RefreshCellAssetStateUseCase, @@ -2888,20 +2634,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideGetAssetSizeLimitUseCase(userScope: UserScope): GetAssetSizeLimitUseCase = userScope.getAssetSizeLimit - @Provides - fun provideHandleUriAssetUseCase( - getAssetSizeLimitUseCase: GetAssetSizeLimitUseCase, - fileManager: FileManager, - kaliumFileSystem: KaliumFileSystem, - dispatchers: DispatcherProvider, - ): HandleUriAssetUseCase = - HandleUriAssetUseCase( - getAssetSizeLimit = getAssetSizeLimitUseCase, - fileManager = fileManager, - kaliumFileSystem = kaliumFileSystem, - dispatchers = dispatchers, - ) - @Provides fun provideImagesPreviewAssetImporter( handleUriAssetUseCase: HandleUriAssetUseCase, @@ -2948,41 +2680,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideRemoveConversationFromFolderUseCase(conversationScope: ConversationScope): RemoveConversationFromFolderUseCase = conversationScope.removeConversationFromFolder - @Provides - @Suppress("LongParameterList") - fun provideGetConversationsFromSearchUseCase( - useCase: GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase, - getFavoriteFolderUseCase: GetFavoriteFolderUseCase, - observeConversationsFromFolder: ObserveConversationsFromFolderUseCase, - userTypeMapper: UserTypeMapper, - dispatchers: DispatcherProvider, - getSelfUser: GetSelfUserUseCase, - uiTextResolver: UiTextResolver, - ): GetConversationsFromSearchUseCase = - GetConversationsFromSearchUseCase( - useCase = useCase, - getFavoriteFolderUseCase = getFavoriteFolderUseCase, - observeConversationsFromFromFolder = observeConversationsFromFolder, - userTypeMapper = userTypeMapper, - dispatchers = dispatchers, - getSelfUser = getSelfUser, - uiTextResolver = uiTextResolver, - ) - - @Provides - fun provideGetConversationMessagesFromSearchUseCase( - getMessagesSearch: GetPaginatedFlowOfMessagesBySearchQueryAndConversationIdUseCase, - getUsersForMessage: GetUsersForMessageUseCase, - messageMapper: MessageMapper, - dispatchers: DispatcherProvider, - ): GetConversationMessagesFromSearchUseCase = - GetConversationMessagesFromSearchUseCase( - getMessagesSearch = getMessagesSearch, - getUsersForMessage = getUsersForMessage, - messageMapper = messageMapper, - dispatchers = dispatchers, - ) - @Provides fun provideObserveSelfDeletionTimerSettingsForConversationUseCase( @KaliumCoreLogic coreLogic: CoreLogic, From e8fa8dd4fd43ade3dd27fed9000442ca97ceeeee Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 13:15:25 +0200 Subject: [PATCH 29/46] refactor: migrate simple gateway bindings to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 14 +++----- .../android/ui/WireActivityIntentGateway.kt | 14 +------- .../android/ui/debug/DebugDataInfoProvider.kt | 14 -------- .../debug/ExportObfuscatedCopyFileGateway.kt | 18 ++--------- .../cells/ui/CellFileExternalActionsModule.kt | 32 ------------------- 5 files changed, 7 insertions(+), 85 deletions(-) delete mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActionsModule.kt 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 index 7c1dd63a59e..60582e42209 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -51,7 +51,6 @@ 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.feature.cells.util.FileHelper import com.wire.android.media.audiomessage.AudioFocusHelper import com.wire.android.media.audiomessage.AudioMessageViewModelFactory import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer @@ -2601,8 +2600,8 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset FileSizeFormatter(context) @Provides - fun provideCellFileExternalActions(fileHelper: FileHelper): CellFileExternalActions = - AndroidCellFileExternalActions(fileHelper) + fun provideCellFileExternalActions(androidCellFileExternalActions: AndroidCellFileExternalActions): CellFileExternalActions = + androidCellFileExternalActions @Provides fun provideCellAssetRefreshHelper( @@ -2754,13 +2753,8 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset @Provides fun provideExportObfuscatedCopyFileGateway( - fileManager: FileManager, - dispatchers: DispatcherProvider, - ): ExportObfuscatedCopyFileGateway = - AndroidExportObfuscatedCopyFileGateway( - fileManager = fileManager, - dispatcher = dispatchers, - ) + gateway: AndroidExportObfuscatedCopyFileGateway, + ): ExportObfuscatedCopyFileGateway = gateway @Provides fun provideWorkManager(@ApplicationContext context: Context): WorkManager = diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityIntentGateway.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityIntentGateway.kt index 0f39a2603eb..db0c85e0577 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityIntentGateway.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityIntentGateway.kt @@ -23,12 +23,7 @@ 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.Binds import dagger.Lazy -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import javax.inject.Inject data class WireActivityIntentContent( val dataUri: String?, @@ -41,7 +36,7 @@ interface WireActivityIntentGateway { suspend fun parseAutomatedLogin(intentContent: WireActivityIntentContent?): AutomatedLoginViaSSO? } -class AndroidWireActivityIntentGateway @Inject constructor( +class AndroidWireActivityIntentGateway( private val deepLinkProcessor: Lazy, private val intentsProcessor: Lazy, ) : WireActivityIntentGateway { @@ -59,10 +54,3 @@ fun Intent.toWireActivityIntentContent(): WireActivityIntentContent = action = action, automatedLogin = getStringExtra(IntentsProcessor.AUTOMATED_LOGIN), ) - -@Module -@InstallIn(ViewModelComponent::class) -interface WireActivityIntentGatewayModule { - @Binds - fun bindWireActivityIntentGateway(gateway: AndroidWireActivityIntentGateway): WireActivityIntentGateway -} 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 index d25eed7d699..34a4c5947f0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt @@ -20,10 +20,6 @@ package com.wire.android.ui.debug import android.content.Context import com.wire.android.util.getDeviceIdString import com.wire.android.util.getGitBuildId -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @@ -38,13 +34,3 @@ class AndroidDebugDataInfoProvider @Inject constructor( override fun deviceId(): String? = context.getDeviceIdString() override fun gitBuildId(): String = context.getGitBuildId() } - -@Module -@InstallIn(ViewModelComponent::class) -interface DebugDataInfoProviderModule { - - @Binds - fun bindDebugDataInfoProvider( - provider: AndroidDebugDataInfoProvider - ): DebugDataInfoProvider -} 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 index 0ca14fa97e1..7a85d94ce99 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyFileGateway.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/ExportObfuscatedCopyFileGateway.kt @@ -21,21 +21,16 @@ 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 dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dev.zacsweers.metro.Inject as MetroInject +import dev.zacsweers.metro.Inject import kotlinx.coroutines.withContext import okio.Path -import javax.inject.Inject interface ExportObfuscatedCopyFileGateway { suspend fun shareCopy(path: Path, assetName: String?) suspend fun saveCopy(path: Path, destinationUri: String) } -class AndroidExportObfuscatedCopyFileGateway @Inject @MetroInject constructor( +class AndroidExportObfuscatedCopyFileGateway @Inject constructor( private val fileManager: FileManager, private val dispatcher: DispatcherProvider, ) : ExportObfuscatedCopyFileGateway { @@ -48,12 +43,3 @@ class AndroidExportObfuscatedCopyFileGateway @Inject @MetroInject constructor( fileManager.copyToUri(path, destinationUri.toUri(), dispatcher) } } - -@Module -@InstallIn(ViewModelComponent::class) -interface ExportObfuscatedCopyFileGatewayModule { - @Binds - fun bindExportObfuscatedCopyFileGateway( - gateway: AndroidExportObfuscatedCopyFileGateway - ): ExportObfuscatedCopyFileGateway -} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActionsModule.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActionsModule.kt deleted file mode 100644 index e7c30e69c4e..00000000000 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellFileExternalActionsModule.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Wire - * Copyright (C) 2026 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.android.feature.cells.ui - -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal interface CellFileExternalActionsModule { - @Binds - fun bindCellFileExternalActions( - androidCellFileExternalActions: AndroidCellFileExternalActions, - ): CellFileExternalActions -} From 7e1a30bd61ec10a841dbbf45dfff6253646677b3 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 13:20:58 +0200 Subject: [PATCH 30/46] chore: remove unused initializer entry point --- .../initializer/InitializerEntryPoint.kt | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt diff --git a/app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt b/app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt deleted file mode 100644 index 365ce3eac6b..00000000000 --- a/app/src/main/kotlin/com/wire/android/initializer/InitializerEntryPoint.kt +++ /dev/null @@ -1,37 +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.initializer - -import android.content.Context -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent - -@EntryPoint -@InstallIn(SingletonComponent::class) -interface InitializerEntryPoint { - - 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) - } - } -} From 9bf64568cee3789a9a3c248e9039a7786778333a Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 13:30:31 +0200 Subject: [PATCH 31/46] refactor: migrate broadcast receiver dependencies to Metro --- .../wire/android/di/metro/WireMetroGraph.kt | 10 +++++ .../BroadcastReceiverDependencies.kt | 40 ++++++++++++++----- 2 files changed, 40 insertions(+), 10 deletions(-) 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 index 60582e42209..b9b49fe0b2d 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -27,6 +27,7 @@ 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 @@ -770,15 +771,24 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset val proximitySensorManager: ProximitySensorManager val servicesManager: ServicesManager val callNotificationManager: CallNotificationManager + val conversationAudioMessagePlayer: ConversationAudioMessagePlayer 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 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 index a5b5efb94c9..4b0ba1b7c0e 100644 --- a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/BroadcastReceiverDependencies.kt +++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/BroadcastReceiverDependencies.kt @@ -22,6 +22,7 @@ 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 @@ -31,14 +32,8 @@ 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 dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope -@EntryPoint -@InstallIn(SingletonComponent::class) interface BroadcastReceiverDependencies { @KaliumCoreLogic fun coreLogic(): CoreLogic @@ -67,7 +62,32 @@ interface BroadcastReceiverDependencies { } val Context.broadcastReceiverDependencies: BroadcastReceiverDependencies - get() = EntryPointAccessors.fromApplication( - applicationContext, - BroadcastReceiverDependencies::class.java - ) + 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 + } + } From e84bfc0f88bef2337e427a3b67dfd7bea3d199bb Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 13:39:49 +0200 Subject: [PATCH 32/46] refactor: move location picker helper to Metro --- .../location/LocationPickerHelperFlavor.kt | 3 ++- .../wire/android/di/metro/WireMetroGraph.kt | 22 +++++++++++++------ .../location/LocationPickerHelper.kt | 3 ++- .../location/LocationPickerHelperFlavor.kt | 3 ++- 4 files changed, 21 insertions(+), 10 deletions(-) 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/di/metro/WireMetroGraph.kt b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt index b9b49fe0b2d..5ecf90c52cb 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -18,6 +18,7 @@ package com.wire.android.di.metro import android.content.Context +import android.location.Geocoder import android.media.AudioAttributes import android.media.AudioManager import android.media.MediaPlayer @@ -120,6 +121,7 @@ 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 @@ -181,7 +183,7 @@ 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.LocationPickerHelperFlavor +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 @@ -908,6 +910,18 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideLoginSSOSessionExceptionClassifier(): LoginSSOSessionExceptionClassifier = LoginSSOSessionExceptionClassifier() + @Provides + fun provideCurrentTimestampProvider(): CurrentTimestampProvider = + { System.currentTimeMillis() } + + @Provides + fun provideGeocoder(@ApplicationContext context: Context): Geocoder = + Geocoder(context) + + @Provides + fun provideLocationPickerParameters(): LocationPickerParameters = + LocationPickerParameters() + @Provides fun provideLockCodeTimeManager(entryPoint: WireMetroHiltEntryPoint): LockCodeTimeManager = entryPoint.lockCodeTimeManager() @@ -920,10 +934,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideNotificationManagerCompat(@ApplicationContext context: Context): NotificationManagerCompat = NotificationManagerCompat.from(context) - @Provides - fun provideLocationPickerHelperFlavor(entryPoint: WireMetroHiltEntryPoint): LocationPickerHelperFlavor = - entryPoint.locationPickerHelperFlavor() - @Provides fun provideLogFileWriter(entryPoint: WireMetroHiltEntryPoint): LogFileWriter = entryPoint.logFileWriter() @@ -3078,8 +3088,6 @@ interface WireMetroHiltEntryPoint { fun conversationAudioMessagePlayer(): ConversationAudioMessagePlayer - fun locationPickerHelperFlavor(): LocationPickerHelperFlavor - fun logFileWriter(): LogFileWriter fun syncLifecycleManager(): SyncLifecycleManager 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..1ee0cda8338 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 @@ -32,6 +32,7 @@ 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 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/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, From 97e6dedb0dec682c28ce3e2c3701e2dc989b328b Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 13:42:09 +0200 Subject: [PATCH 33/46] refactor: provide log file writer through Metro --- .../com/wire/android/di/metro/WireMetroGraph.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 index 5ecf90c52cb..b78853e0994 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -290,6 +290,8 @@ 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 @@ -935,8 +937,14 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset NotificationManagerCompat.from(context) @Provides - fun provideLogFileWriter(entryPoint: WireMetroHiltEntryPoint): LogFileWriter = - entryPoint.logFileWriter() + fun provideLogFileWriter(@ApplicationContext context: Context): LogFileWriter { + val logsDirectory = LogFileWriter.logsDirectory(context) + return if (BuildConfig.USE_ASYNC_FLUSH_LOGGING) { + LogFileWriterV2Impl(logsDirectory) + } else { + LogFileWriterV1Impl(logsDirectory) + } + } @Provides fun provideAccountSwitchUseCase(entryPoint: WireMetroHiltEntryPoint): AccountSwitchUseCase = @@ -3088,8 +3096,6 @@ interface WireMetroHiltEntryPoint { fun conversationAudioMessagePlayer(): ConversationAudioMessagePlayer - fun logFileWriter(): LogFileWriter - fun syncLifecycleManager(): SyncLifecycleManager fun wireWorkerFactory(): WireWorkerFactory From a2bd79dbfe7513089072eb8e74f34d0bab52b1df Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 13:45:36 +0200 Subject: [PATCH 34/46] chore: remove duplicate audio loudness Hilt provider --- app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt | 5 ----- 1 file changed, 5 deletions(-) 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..26498f32dda 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -31,7 +31,6 @@ 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 @@ -148,10 +147,6 @@ class CoreLogicModule { @Provides fun provideWorkManager(@ApplicationContext applicationContext: Context) = WorkManager.getInstance(applicationContext) - @Provides - fun provideAudioNormalizedLoudnessBuilder(@KaliumCoreLogic coreLogic: CoreLogic): AudioNormalizedLoudnessBuilder = - coreLogic.audioNormalizedLoudnessBuilder - @DefaultWebSocketEnabledByDefault @Provides fun provideDefaultWebSocketEnabledByDefault( From cd578eef16aa3e786afb5dd805cf289e27f3c79d Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 16:45:59 +0200 Subject: [PATCH 35/46] refactor: remove hilt from app tests --- app/build.gradle.kts | 11 +- .../com/wire/android/CustomTestRunner.kt | 2 +- .../kotlin/com/wire/android/HiltTestApp.kt | 23 - .../com/wire/android/TestCoreLogicModule.kt | 127 ----- .../com/wire/android/WireActivityTest.kt | 9 +- .../com/wire/android/WireApplication.kt | 107 ++-- .../config/NomadProfilesFeatureConfig.kt | 3 +- .../wire/android/datastore/GlobalDataStore.kt | 2 +- .../datastore/UserDataStoreProvider.kt | 2 +- .../kotlin/com/wire/android/di/AppModule.kt | 112 ----- .../com/wire/android/di/CoreLogicModule.kt | 476 ------------------ .../com/wire/android/di/CoroutineScope.kt | 42 -- .../wire/android/di/KaliumConfigsModule.kt | 49 -- .../com/wire/android/di/LogWriterModule.kt | 29 -- .../android/di/ManagedConfigurationsModule.kt | 83 --- .../com/wire/android/di/ViewModelScoped.kt | 105 ++-- .../android/di/accountScoped/DebugModule.kt | 20 - .../metro/MetroViewModelMigrationTemplate.kt | 8 +- .../wire/android/di/metro/WireMetroGraph.kt | 394 ++++++++++----- .../emm/ManagedConfigurationsReporter.kt | 2 +- .../android/feature/DisableAppLockUseCase.kt | 2 - .../ConversationAudioMessagePlayer.kt | 2 +- .../audiomessage/RecordAudioMessagePlayer.kt | 2 - .../wire/android/services/ServicesManager.kt | 3 +- .../analytics/IsAnalyticsAvailableUseCase.kt | 2 - .../calling/common/ProximitySensorManager.kt | 7 +- .../ui/calling/usecase/HangUpCallUseCase.kt | 2 - .../android/ui/debug/DebugDataInfoProvider.kt | 4 +- .../location/GeocoderHelper.kt | 3 +- .../location/LocationPickerHelper.kt | 2 +- .../home/settings/backup/BackupFileGateway.kt | 3 +- .../login/ValidateEmailOrSSOCodeUseCase.kt | 2 - .../ui/settings/about/AboutThisAppModule.kt | 20 - .../com/wire/android/util/FileManager.kt | 2 +- .../wire/android/util/ScreenStateObserver.kt | 2 +- .../wire/android/util/UserAgentProvider.kt | 2 +- .../wire/android/util/ui/UiTextResolver.kt | 2 +- .../NomadLogoutReceiverTest.kt | 17 +- .../android/ui/WireActivityViewModelTest.kt | 1 - .../ConnectionActionButtonViewModelTest.kt | 28 +- .../preview/ImagesPreviewViewModelTest.kt | 39 +- .../recordaudio/RecordAudioViewModelTest.kt | 2 +- .../home/BackupAndRestoreViewModelTest.kt | 2 + .../debug/DebugDataOptionsViewModelTest.kt | 38 +- .../ImportMediaAuthenticatedViewModelTest.kt | 2 +- .../src/main/kotlin/HiltConventionPlugin.kt | 5 - core/di/build.gradle.kts | 2 +- .../com/wire/android/di/KaliumCoreLogic.kt | 4 + core/media/build.gradle.kts | 2 +- core/notification/build.gradle.kts | 2 +- core/ui-common/build.gradle.kts | 7 +- .../wire/android/util/FileSizeFormatter.kt | 2 +- features/cells/build.gradle.kts | 5 +- .../ui/AndroidCellFileExternalActions.kt | 5 +- .../android/feature/cells/util/FileHelper.kt | 7 +- features/meetings/build.gradle.kts | 7 +- features/sync/build.gradle.kts | 6 +- .../android/sync/MonitorSyncWorkUseCase.kt | 2 +- gradle/libs.versions.toml | 4 +- 59 files changed, 493 insertions(+), 1364 deletions(-) delete mode 100644 app/src/androidTest/kotlin/com/wire/android/HiltTestApp.kt delete mode 100644 app/src/androidTest/kotlin/com/wire/android/TestCoreLogicModule.kt delete mode 100644 app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt delete mode 100644 app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3fe03d4ad2c..572c6d62e30 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) @@ -274,14 +273,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 +327,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/HiltTestApp.kt b/app/src/androidTest/kotlin/com/wire/android/HiltTestApp.kt deleted file mode 100644 index 1983d2b885d..00000000000 --- a/app/src/androidTest/kotlin/com/wire/android/HiltTestApp.kt +++ /dev/null @@ -1,23 +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 dagger.hilt.android.testing.CustomTestApplication - -@CustomTestApplication(BaseApp::class) -interface HiltTestApp 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/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/config/NomadProfilesFeatureConfig.kt b/app/src/main/kotlin/com/wire/android/config/NomadProfilesFeatureConfig.kt index 726cf5e17c6..d77e8a38bfa 100644 --- a/app/src/main/kotlin/com/wire/android/config/NomadProfilesFeatureConfig.kt +++ b/app/src/main/kotlin/com/wire/android/config/NomadProfilesFeatureConfig.kt @@ -20,8 +20,7 @@ package com.wire.android.config import com.wire.android.BuildConfig import dev.zacsweers.metro.Inject -import javax.inject.Inject as HiltInject -class NomadProfilesFeatureConfig @Inject @HiltInject constructor() { +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 26498f32dda..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,62 +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.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) @@ -86,424 +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) - - @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/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 fc41c82ffce..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.LogFileWriter -import com.wire.android.util.logging.LogFileWriterV1Impl -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 c4213b3cd80..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,86 +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 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, - serverConfigProvider: ServerConfigProvider, - ): 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 - serverConfigProvider.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 9079bfe7a5e..8573c5d3373 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,18 @@ 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 kotlin.time.Duration /** @@ -42,18 +50,22 @@ 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 > 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().create(arguments) + }, + ) } -@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") +@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER", "UnusedParameter") @Composable inline fun > wireViewModelScoped( @@ -64,16 +76,17 @@ 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().create(arguments) + }, + ) } } @@ -90,45 +103,43 @@ 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>().create(EmptyScopedArgs) + }, + ) } -@Deprecated( - message = "Use wireViewModelScoped to avoid exposing the DI implementation in local APIs.", - replaceWith = ReplaceWith("wireViewModelScoped(arguments, clearDelay)") -) -@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") +@PublishedApi @Composable -inline fun > - hiltViewModelScoped(arguments: R, clearDelay: Duration? = null): S where T : ViewModel, T : S = - wireViewModelScoped(arguments = arguments, clearDelay = clearDelay) +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() + } + } + } +} -@Deprecated( - message = "Use wireViewModelScoped to avoid exposing the DI implementation in local APIs.", - replaceWith = ReplaceWith("wireViewModelScoped(arguments, keyInScopeResolver, clearDelay)") -) -@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") -@Composable -inline fun > - hiltViewModelScoped( - arguments: R, - noinline keyInScopeResolver: KeyInScopeResolver, - clearDelay: Duration? = null, -): S where T : ViewModel, T : S = - wireViewModelScoped( - arguments = arguments, - keyInScopeResolver = keyInScopeResolver, - clearDelay = clearDelay - ) +@PublishedApi +internal inline fun WireMetroGraph.metroFactory(): F = + this::class.java.methods + .firstOrNull { method -> + method.parameterCount == 0 && F::class.java.isAssignableFrom(method.returnType) + } + ?.invoke(this) as? F + ?: error("No Metro factory matching ${F::class.qualifiedName} was provided.") -@Deprecated( - message = "Use wireViewModelScoped to avoid exposing the DI implementation in local APIs.", - replaceWith = ReplaceWith("wireViewModelScoped()") -) -@Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER") -@Composable -inline fun hiltViewModelScoped(): S where T : ViewModel, T : S = - wireViewModelScoped() +@PublishedApi +internal data object EmptyScopedArgs : ScopedArgs { + override val key: Any? = null +} val espresso get() = try { 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 35ae70d8969..00000000000 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt +++ /dev/null @@ -1,20 +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 - -// Debug account-scoped providers are owned by WireMetroGraph. 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 index 2eeb0506456..a1de35ed068 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelMigrationTemplate.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/MetroViewModelMigrationTemplate.kt @@ -21,7 +21,7 @@ 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 Hilt until the Metro graph is wired into the app. Prefer this shape: + * creation on the current Android runtime until the Metro graph is wired into the app. Prefer this shape: * * ``` * @Inject @@ -47,12 +47,12 @@ package com.wire.android.di.metro * * Rules for each migrated ViewModel: * - keep the ViewModel constructor platform-neutral: no `SavedStateHandle`, `NavController`, Compose destination args, - * Android `Context`, or direct Hilt-only creation contract; + * 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 Hilt and Metro coexist. + * - 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: * @@ -82,6 +82,6 @@ package com.wire.android.di.metro * - 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 Hilt annotations stay until the runtime bridge is ready. + * 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 index b78853e0994..ad9cb8b73ea 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -17,6 +17,7 @@ */ package com.wire.android.di.metro +import android.app.NotificationManager import android.content.Context import android.location.Geocoder import android.media.AudioAttributes @@ -43,20 +44,24 @@ 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.AudioFocusHelper import com.wire.android.media.audiomessage.AudioMessageViewModelFactory import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer -import com.wire.android.media.audiomessage.RecordAudioMessagePlayer import com.wire.android.media.CallRinger import com.wire.android.media.PingRinger import com.wire.android.mapper.MessageContentMapper @@ -74,6 +79,8 @@ 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 @@ -276,13 +283,14 @@ 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.FileSizeFormatter 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 @@ -519,6 +527,7 @@ 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 @@ -610,16 +619,14 @@ 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 dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent +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() @@ -772,10 +779,18 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset 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 @@ -798,19 +813,34 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset val currentAccount: UserId @Provides - fun provideWireMetroHiltEntryPoint( + @SingleIn(WireMetroScope::class) + fun provideGlobalDataStore( @ApplicationContext context: Context, - ): WireMetroHiltEntryPoint = - EntryPointAccessors.fromApplication(context, WireMetroHiltEntryPoint::class.java) + ): GlobalDataStore = GlobalDataStore(context) @Provides - fun provideGlobalDataStore( + @SingleIn(WireMetroScope::class) + fun provideUserAgentProvider( @ApplicationContext context: Context, - ): GlobalDataStore = GlobalDataStore(context) + ): UserAgentProvider = + UserAgentProvider(context) @Provides @KaliumCoreLogic - fun provideKaliumCoreLogic(entryPoint: WireMetroHiltEntryPoint): CoreLogic = entryPoint.coreLogic() + @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 @@ -820,8 +850,11 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset } @Provides - fun provideUserDataStoreProvider(entryPoint: WireMetroHiltEntryPoint): UserDataStoreProvider = - entryPoint.userDataStoreProvider() + @SingleIn(WireMetroScope::class) + fun provideUserDataStoreProvider( + @ApplicationContext context: Context, + ): UserDataStoreProvider = + UserDataStoreProvider(context) @Provides fun provideCurrentAccountUserDataStore( @@ -843,24 +876,52 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset } @Provides - fun provideDispatchers(entryPoint: WireMetroHiltEntryPoint): DispatcherProvider = entryPoint.dispatcherProvider() + fun provideDispatchers(): DispatcherProvider = DefaultDispatcherProvider() + + @Provides + fun provideAutomatedLoginManager(): AutomatedLoginManager = + AutomatedLoginManager() + + @Provides + fun provideServerConfigProvider(): ServerConfigProvider = + ServerConfigProvider() @Provides - fun provideAutomatedLoginManager(entryPoint: WireMetroHiltEntryPoint): AutomatedLoginManager = - entryPoint.automatedLoginManager() + fun provideAndroidUserContextProvider(): AndroidUserContextProvider = + AndroidUserContextProviderImpl() @Provides - fun provideManagedConfigurationsManager(entryPoint: WireMetroHiltEntryPoint): ManagedConfigurationsManager = - entryPoint.managedConfigurationsManager() + 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) + serverConfigProvider.getDefaultServerConfig(null) } @Named("ssoCodeConfig") @@ -925,18 +986,103 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset LocationPickerParameters() @Provides - fun provideLockCodeTimeManager(entryPoint: WireMetroHiltEntryPoint): LockCodeTimeManager = - entryPoint.lockCodeTimeManager() - - @Provides - fun provideWireNotificationManager(entryPoint: WireMetroHiltEntryPoint): WireNotificationManager = - entryPoint.wireNotificationManager() + @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) { @@ -947,10 +1093,7 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset } @Provides - fun provideAccountSwitchUseCase(entryPoint: WireMetroHiltEntryPoint): AccountSwitchUseCase = - entryPoint.accountSwitchUseCase() - - @Provides + @SingleIn(WireMetroScope::class) fun provideAnonymousAnalyticsManager(): AnonymousAnalyticsManager = AnonymousAnalyticsManagerImpl @@ -963,8 +1106,13 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset } @Provides - fun provideLoginTypeSelector(entryPoint: WireMetroHiltEntryPoint): LoginTypeSelector = - entryPoint.loginTypeSelector() + 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 = @@ -1058,16 +1206,23 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset override fun get(): AccountSwitchUseCase = accountSwitch } - @Provides - fun provideServicesManager(entryPoint: WireMetroHiltEntryPoint): ServicesManager = - entryPoint.servicesManager() - @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 { @@ -1320,8 +1475,9 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset @ApplicationScope @Provides - fun provideApplicationCoroutineScope(entryPoint: WireMetroHiltEntryPoint): CoroutineScope = - entryPoint.applicationScope() + @SingleIn(WireMetroScope::class) + fun provideApplicationCoroutineScope(dispatcherProvider: DispatcherProvider): CoroutineScope = + CoroutineScope(SupervisorJob() + dispatcherProvider.default()) @Provides fun provideMusicMediaPlayer(): MediaPlayer = @@ -1339,34 +1495,24 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset context.getSystemService(Context.AUDIO_SERVICE) as AudioManager @Provides - fun provideRecordAudioMessagePlayer( - @ApplicationContext context: Context, - mediaPlayer: MediaPlayer, - audioFocusHelper: AudioFocusHelper, - @ApplicationScope applicationScope: CoroutineScope, - ): RecordAudioMessagePlayer = - RecordAudioMessagePlayer( - context = context, - audioMediaPlayer = mediaPlayer, - audioFocusHelper = audioFocusHelper, - scope = applicationScope, - ) - - @Provides - fun provideConversationAudioMessagePlayer(entryPoint: WireMetroHiltEntryPoint): ConversationAudioMessagePlayer = - entryPoint.conversationAudioMessagePlayer() - - @Provides + @SingleIn(WireMetroScope::class) fun provideScreenStateObserver(@ApplicationContext context: Context): ScreenStateObserver = ScreenStateObserver(context) @Provides - fun provideCurrentScreenManager(entryPoint: WireMetroHiltEntryPoint): CurrentScreenManager = - entryPoint.currentScreenManager() + @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(entryPoint: WireMetroHiltEntryPoint): SwitchAccountObserver = - entryPoint.switchAccountObserver() + fun provideSwitchAccountObserver(): SwitchAccountObserver = + SwitchAccountObserver() @Provides fun provideNetworkUtil(@ApplicationContext context: Context): NetworkUtil = @@ -1405,8 +1551,19 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset ) @Provides - fun provideProximitySensorManager(entryPoint: WireMetroHiltEntryPoint): ProximitySensorManager = - entryPoint.proximitySensorManager() + @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( @@ -1633,10 +1790,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideImageUtil(): ImageUtil = ImageUtil - @Provides - fun providePingRinger(@ApplicationContext context: Context): PingRinger = - PingRinger(context) - @Provides fun provideTempWritableAttachmentUriProvider( fileManager: FileManager, @@ -2623,10 +2776,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideGetWireCellConfigurationUseCase(cellsScope: CellsScope): GetWireCellConfigurationUseCase = cellsScope.getCellConfig - @Provides - fun provideFileSizeFormatter(@ApplicationContext context: Context): FileSizeFormatter = - FileSizeFormatter(context) - @Provides fun provideCellFileExternalActions(androidCellFileExternalActions: AndroidCellFileExternalActions): CellFileExternalActions = androidCellFileExternalActions @@ -2785,13 +2934,53 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset ): 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 @@ -2941,10 +3130,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset getPrivateAsset = getMessageAsset, ).newImageLoader() - @Provides - fun provideCallRinger(@ApplicationContext context: Context): CallRinger = - CallRinger(context) - @Provides @Suppress("LongParameterList") fun provideHangUpCallUseCase( @@ -2968,10 +3153,6 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset callRinger = callRinger, ) - @Provides - fun provideCallNotificationManager(entryPoint: WireMetroHiltEntryPoint): CallNotificationManager = - entryPoint.callNotificationManager() - @Provides fun provideCallServiceManager(@KaliumCoreLogic coreLogic: CoreLogic): CallServiceManager = CallServiceManager(coreLogic) @@ -3063,49 +3244,16 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset } fun createWireMetroGraph(context: Context): WireMetroGraph = - createGraphFactory().create(context.applicationContext) - -@EntryPoint -@InstallIn(SingletonComponent::class) -@Suppress("TooManyFunctions") -interface WireMetroHiltEntryPoint { - @KaliumCoreLogic - fun coreLogic(): CoreLogic - - fun userDataStoreProvider(): UserDataStoreProvider + WireMetroGraphProvider.get(context.applicationContext) - fun dispatcherProvider(): DispatcherProvider +private object WireMetroGraphProvider { + @Volatile + private var graph: WireMetroGraph? = null - fun automatedLoginManager(): AutomatedLoginManager - - fun currentScreenManager(): CurrentScreenManager - - fun switchAccountObserver(): SwitchAccountObserver - - fun loginTypeSelector(): LoginTypeSelector - - fun proximitySensorManager(): ProximitySensorManager - - fun managedConfigurationsManager(): ManagedConfigurationsManager - - fun lockCodeTimeManager(): LockCodeTimeManager - - fun wireNotificationManager(): WireNotificationManager - - fun callNotificationManager(): CallNotificationManager - - fun conversationAudioMessagePlayer(): ConversationAudioMessagePlayer - - fun syncLifecycleManager(): SyncLifecycleManager - - fun wireWorkerFactory(): WireWorkerFactory - - fun globalObserversManager(): GlobalObserversManager - - @ApplicationScope - fun applicationScope(): CoroutineScope - - fun accountSwitchUseCase(): AccountSwitchUseCase - - fun servicesManager(): ServicesManager + 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/DisableAppLockUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/DisableAppLockUseCase.kt index 879dfc81faf..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,10 @@ 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 @MetroInject constructor( private val dataStore: GlobalDataStore, private val observeIsAppLockEditableUseCase: ObserveIsAppLockEditableUseCase 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 8be10c8c76f..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 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 7793c265875..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 @@ -37,7 +36,6 @@ import java.io.File import dev.zacsweers.metro.Inject as MetroInject import javax.inject.Inject -@ViewModelScoped class RecordAudioMessagePlayer @Inject @MetroInject constructor( private val context: Context, private val audioMediaPlayer: MediaPlayer, 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/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/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/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/debug/DebugDataInfoProvider.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt index 34a4c5947f0..8691d612779 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataInfoProvider.kt @@ -20,8 +20,8 @@ package com.wire.android.ui.debug import android.content.Context import com.wire.android.util.getDeviceIdString import com.wire.android.util.getGitBuildId -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject +import com.wire.android.di.ApplicationContext +import dev.zacsweers.metro.Inject interface DebugDataInfoProvider { fun deviceId(): String? 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 c099843a3f9..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 @@ -20,9 +20,8 @@ package com.wire.android.ui.home.messagecomposer.location import android.location.Geocoder import android.location.Location import dev.zacsweers.metro.Inject -import javax.inject.Inject as HiltInject -class GeocoderHelper @Inject @HiltInject constructor(private val geocoder: Geocoder) { +class GeocoderHelper @Inject constructor(private val geocoder: Geocoder) { @Suppress("TooGenericExceptionCaught") fun getGeoLocatedAddress(location: Location): GeoLocatedAddress = 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 1ee0cda8338..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,7 @@ 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 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 index 0e5007cdb7e..8d5ce22a3d9 100644 --- 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 @@ -22,6 +22,7 @@ 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 @@ -32,7 +33,7 @@ interface BackupFileGateway { suspend fun deleteImportedBackup(path: Path) } -class AndroidBackupFileGateway( +class AndroidBackupFileGateway @Inject constructor( private val fileManager: FileManager, private val kaliumFileSystem: KaliumFileSystem, private val dispatcher: DispatcherProvider, 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/settings/about/AboutThisAppModule.kt b/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppModule.kt deleted file mode 100644 index e773e737a14..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/settings/about/AboutThisAppModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Wire - * Copyright (C) 2026 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.android.ui.settings.about - -// About-this-app bindings are provided by the Metro graph. 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/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/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 84610074234..fe257bf47d0 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -1072,7 +1072,6 @@ class WireActivityViewModelTest { @MockK lateinit var getServerConfigUseCase: GetServerConfigUseCase - @MockK @MockK lateinit var intentGateway: WireActivityIntentGateway 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/home/conversations/media/preview/ImagesPreviewViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/media/preview/ImagesPreviewViewModelTest.kt index 24fc14d20d0..30e97f60123 100644 --- 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 @@ -17,25 +17,44 @@ */ package com.wire.android.ui.home.conversations.media.preview +import android.app.Application import androidx.core.net.toUri -import com.wire.android.config.CoroutineTestExtension 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.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith +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) -@ExtendWith(CoroutineTestExtension::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) @@ -47,7 +66,7 @@ class ImagesPreviewViewModelTest { runCurrent() - assertEquals(listOf("content://asset/first", "content://asset/second"), arrangement.assetImporter.importedUris) + assertEquals(2, arrangement.assetImporter.importedUris.size) assertEquals(listOf(firstAsset, secondAsset), viewModel.viewState.assetBundleList) assertFalse(viewModel.viewState.isLoading) } @@ -62,7 +81,7 @@ class ImagesPreviewViewModelTest { runCurrent() - assertEquals(listOf("content://asset/clean", "content://asset/broken"), arrangement.assetImporter.importedUris) + assertEquals(2, arrangement.assetImporter.importedUris.size) assertEquals(listOf(importedAsset), viewModel.viewState.assetBundleList) assertFalse(viewModel.viewState.isLoading) } @@ -96,8 +115,8 @@ class ImagesPreviewViewModelTest { private class Arrangement { val assetImporter = FakeImagesPreviewAssetImporter() - fun withImportedAsset(uri: String, importedMediaAsset: ImportedMediaAsset?) = apply { - assetImporter.assets[uri] = importedMediaAsset + fun withImportedAsset(@Suppress("UNUSED_PARAMETER") uri: String, importedMediaAsset: ImportedMediaAsset?) = apply { + assetImporter.assets += importedMediaAsset } fun arrange( @@ -117,11 +136,11 @@ class ImagesPreviewViewModelTest { private class FakeImagesPreviewAssetImporter : ImagesPreviewAssetImporter { val importedUris = mutableListOf() - val assets = mutableMapOf() + val assets = mutableListOf() override suspend fun importAsset(uri: String): ImportedMediaAsset? { importedUris += uri - return assets[uri] + return assets.removeAt(0) } } } 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 bc7f487661c..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 @@ -423,7 +423,7 @@ class RecordAudioViewModelTest { fun arrange() = this to viewModel - private class FakeRecordAudioFileGateway : RecordAudioFileGateway { + class FakeRecordAudioFileGateway : RecordAudioFileGateway { var generateAudioFileWithEffectsCalls = 0 private set 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 fd4d4163764..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 @@ -544,6 +544,8 @@ class BackupAndRestoreViewModelTest { } fun withRequestedPasswordDialog() = apply { + viewModel.latestImportedBackupTempPath = + fakeKaliumFileSystem.tempFilePath(BackupAndRestoreViewModel.TEMP_IMPORTED_BACKUP_FILE_NAME) viewModel.state = viewModel.state.copy(restoreFileValidation = RestoreFileValidation.PasswordRequired) } 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 c3d880eace6..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 @@ -75,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() @@ -92,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() @@ -109,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() @@ -126,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() @@ -143,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() @@ -153,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() @@ -163,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() @@ -173,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() @@ -183,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() @@ -193,7 +193,7 @@ 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() @@ -202,7 +202,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given debug data info is available, view state should contain debug id and commitish`() = runTest { - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withDebugDataInfo( deviceId = "fakeDeviceId", gitBuildId = "fakeGitBuildId" @@ -216,7 +216,7 @@ class DebugDataOptionsViewModelTest { @Test fun `given server config failure, view state should have default values`() = runTest { // given - val (_, viewModel) = DebugDataOptionsHiltArrangement() + val (_, viewModel) = DebugDataOptionsArrangement() .withServerConfigError() .arrange() @@ -227,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() @@ -243,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() @@ -258,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() @@ -267,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() @@ -277,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) @@ -287,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() @@ -298,7 +298,7 @@ class DebugDataOptionsViewModelTest { } } -internal class DebugDataOptionsHiltArrangement { +internal class DebugDataOptionsArrangement { private var debugDataInfoProvider = FakeDebugDataInfoProvider() 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 dde3cc6e3e4..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 @@ -158,7 +158,7 @@ class ImportMediaAuthenticatedViewModelTest { assetType = AttachmentType.GENERIC_FILE, ) - inner class Arrangement { + private inner class Arrangement { @MockK lateinit var getSelfUser: ObserveSelfUserUseCase 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/core/di/build.gradle.kts b/core/di/build.gradle.kts index 07ae41d5a10..9d94ebbddb2 100644 --- a/core/di/build.gradle.kts +++ b/core/di/build.gradle.kts @@ -6,7 +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/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/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/ui-common/build.gradle.kts b/core/ui-common/build.gradle.kts index f43bdc1b395..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) } @@ -37,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/util/FileSizeFormatter.kt b/core/ui-common/src/main/kotlin/com/wire/android/util/FileSizeFormatter.kt index 64842294ef5..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,7 +19,7 @@ package com.wire.android.util import android.content.Context import com.wire.android.ui.common.R -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.di.ApplicationContext import dev.zacsweers.metro.Inject class FileSizeFormatter @Inject constructor( diff --git a/features/cells/build.gradle.kts b/features/cells/build.gradle.kts index 051d9008f62..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) @@ -22,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/AndroidCellFileExternalActions.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/AndroidCellFileExternalActions.kt index 04e88bc9193..18017079ad8 100644 --- 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 @@ -18,11 +18,10 @@ 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 -import dev.zacsweers.metro.Inject as MetroInject -import javax.inject.Inject -class AndroidCellFileExternalActions @Inject @MetroInject constructor( +class AndroidCellFileExternalActions @Inject constructor( private val fileHelper: FileHelper, ) : CellFileExternalActions { 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 3b3c6460db9..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,15 +26,14 @@ 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 dev.zacsweers.metro.Inject as MetroInject -import javax.inject.Inject -class FileHelper @Inject @MetroInject constructor( +class FileHelper @Inject constructor( @ApplicationContext private val context: Context ) { diff --git a/features/meetings/build.gradle.kts b/features/meetings/build.gradle.kts index 9d79d361c9d..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) @@ -20,14 +19,10 @@ dependencies { 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/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/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 4bc1f7a3433..ef02efbaced 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ desugaring = "2.1.5" firebaseBOM = "34.7.0" fragment = "1.5.6" resaca = "5.0.2" +resacaMetro = "5.0.0" metro = "0.12.1" bundlizer = "0.8.0" squareup-javapoet = "1.13.0" @@ -161,6 +162,7 @@ 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" } @@ -194,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" } @@ -228,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 From fe8c32a0aefda0e4a370d7e876129017c05427b5 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 14 May 2026 16:55:16 +0200 Subject: [PATCH 36/46] fix: restore scoped metro view models --- .../com/wire/android/di/ViewModelScoped.kt | 45 ++++++++++++++++--- .../android/ui/common/AddContactButton.kt | 16 ++++--- .../banner/SecurityClassificationBanner.kt | 30 ++++++------- .../ConversationOptionsModalSheetLayout.kt | 6 +-- .../ui/connection/ConnectionActionButton.kt | 15 ++++--- .../wire/android/ui/debug/DebugDataOptions.kt | 6 +-- .../com/wire/android/ui/debug/DebugScreen.kt | 7 ++- .../conversations/UsersTypingIndicator.kt | 16 +++---- .../edit/MessageOptionsModalSheetLayout.kt | 13 +++--- .../conversations/messages/QuotedMessage.kt | 26 +++++++---- .../home/conversations/model/MessageTypes.kt | 14 +++--- .../messagetypes/audio/AudioMessageType.kt | 27 +++++++---- .../messagecomposer/MessageComposeActions.kt | 18 ++++---- .../messagecomposer/MessageComposerInput.kt | 16 ++++--- .../attachments/AdditionalOptionButton.kt | 6 +-- .../recordaudio/RecordAudioComponent.kt | 4 +- .../JoinConversationViaDeepLinkDialog.kt | 7 ++- 17 files changed, 164 insertions(+), 108 deletions(-) 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 8573c5d3373..82e7f1a1543 100644 --- a/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt +++ b/app/src/main/kotlin/com/wire/android/di/ViewModelScoped.kt @@ -30,6 +30,7 @@ import com.sebaslogen.resaca.KeyInScopeResolver 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 /** @@ -52,7 +53,7 @@ interface AssistedViewModelFactory { */ @Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER", "UnusedParameter") @Composable -inline fun > +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 } @@ -60,14 +61,14 @@ inline fun { - metroFactory().create(arguments) + metroFactory().createWith(arguments) }, ) } @Suppress("BOUNDS_NOT_ALLOWED_IF_BOUNDED_BY_TYPE_PARAMETER", "UnusedParameter") @Composable -inline fun > +inline fun wireViewModelScoped( arguments: R, noinline keyInScopeResolver: KeyInScopeResolver, @@ -84,7 +85,7 @@ inline fun { - metroFactory().create(arguments) + metroFactory().createWith(arguments) }, ) } @@ -100,12 +101,12 @@ inline fun wireViewModelScoped(): 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 -> metroViewModelScoped( factory = rememberMetroScopedViewModelFactory { - metroFactory>().create(EmptyScopedArgs) + metroFactory().createWithoutArgs() }, ) } @@ -133,9 +134,39 @@ internal inline fun WireMetroGraph.metroFactory(): F = .firstOrNull { method -> method.parameterCount == 0 && F::class.java.isAssignableFrom(method.returnType) } - ?.invoke(this) as? F + ?.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 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 44fc2238573..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.metro.metroViewModel +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,14 @@ fun AddContactButton( userName: String, modifier: Modifier = Modifier, viewModel: ConnectionActionButtonViewModel = - ConnectionActionButtonArgs(userId, userName).let { args -> - metroViewModel(key = args.key) { - connectionActionButtonViewModelFactory.createImpl(args) - } - }, + wireViewModelScoped< + ConnectionActionButtonViewModelImpl, + ConnectionActionButtonViewModel, + ConnectionActionButtonArgs, + ConnectionActionButtonViewModelFactory + >( + ConnectionActionButtonArgs(userId, userName) + ), ) { val state = viewModel.actionableState() LocalSnackbarHostState.current.collectAndShowSnackbar(snackbarFlow = viewModel.infoMessage) 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 8c556a4f63b..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.metro.metroViewModel +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,13 +56,12 @@ fun SecurityClassificationBannerForConversation( conversationId: ConversationId, modifier: Modifier = Modifier, viewModel: SecurityClassificationViewModel = - SecurityClassificationArgs.Conversation(conversationId).let { args -> - metroViewModel( - key = args.key.toString(), - ) { - securityClassificationViewModelFactory.create(args) - } - } + wireViewModelScoped< + SecurityClassificationViewModelImpl, + SecurityClassificationViewModel, + SecurityClassificationArgs, + SecurityClassificationViewModelFactory + >(SecurityClassificationArgs.Conversation(conversationId)) ) { SecurityClassificationBanner( state = viewModel.state(), @@ -75,13 +74,14 @@ fun SecurityClassificationBannerForUser( userId: UserId, modifier: Modifier = Modifier, viewModel: SecurityClassificationViewModel = - SecurityClassificationArgs.User(id = userId).let { args -> - metroViewModel( - key = args.key.toString(), - ) { - securityClassificationViewModelFactory.create(args) - } - } + wireViewModelScoped< + SecurityClassificationViewModelImpl, + SecurityClassificationViewModel, + SecurityClassificationArgs, + SecurityClassificationViewModelFactory + >( + SecurityClassificationArgs.User(id = userId) + ) ) { SecurityClassificationBanner( state = viewModel.state(), diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt index eba5fabf835..0bd3e2da785 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt @@ -24,7 +24,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.metro.metroViewModel +import com.wire.android.di.wireViewModelScoped import com.wire.android.ui.common.HandleActions import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout import com.wire.android.ui.common.bottomsheet.WireModalSheetState @@ -58,9 +58,7 @@ fun ConversationOptionsModalSheetLayout( onPromoteAdmin: (ConversationId) -> Unit = {}, openConversationDebugMenu: (ConversationId) -> Unit = {}, viewModel: ConversationOptionsMenuViewModel = - metroViewModel { - conversationOptionsMenuViewModelFactory.create() - } + wireViewModelScoped() ) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current 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 7685140fb75..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.metro.metroViewModel +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 @@ -74,11 +74,14 @@ fun ConnectionActionButton( onConnectionRequestIgnored: (String) -> Unit = {}, onOpenConversation: (ConversationId) -> Unit = {}, viewModel: ConnectionActionButtonViewModel = - ConnectionActionButtonArgs(userId, userName).let { args -> - metroViewModel(key = args.key) { - connectionActionButtonViewModelFactory.createImpl(args) - } - }, + wireViewModelScoped< + ConnectionActionButtonViewModelImpl, + ConnectionActionButtonViewModel, + ConnectionActionButtonArgs, + ConnectionActionButtonViewModelFactory + >( + ConnectionActionButtonArgs(userId, userName) + ), ) { LocalSnackbarHostState.current.collectAndShowSnackbar(snackbarFlow = viewModel.infoMessage) val unblockUserDialogState = rememberVisibilityState() 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 def0a7819c7..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.metro.metroViewModel +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,9 +70,7 @@ fun DebugDataOptions( onShowFeatureFlags: () -> Unit, onShowCryptoStats: () -> Unit, viewModel: DebugDataOptionsViewModel = - metroViewModel { - debugDataOptionsViewModelFactory.create() - } + wireViewModelScoped() ) { LocalSnackbarHostState.current.collectAndShowSnackbar(snackbarFlow = viewModel.infoMessage) DebugDataOptionsContent( 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 7c826acd75d..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,9 +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 com.wire.android.di.metro.metroViewModel import com.wire.android.BuildConfig import com.wire.android.R +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 @@ -150,9 +151,7 @@ internal fun UserDebugContent( fun DangerOptions( modifier: Modifier = Modifier, exportObfuscatedCopyViewModel: ExportObfuscatedCopyViewModel = - metroViewModel { - exportObfuscatedCopyViewModelFactory.create() - } + wireViewModelScoped() ) { Column(modifier = modifier) { 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 aed92995938..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.metro.metroViewModel +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,13 +75,12 @@ private const val ANIMATION_SPEED_MILLIS = 1_500 fun UsersTypingIndicatorForConversation( conversationId: ConversationId, viewModel: TypingIndicatorViewModel = - TypingIndicatorArgs(conversationId).let { args -> - metroViewModel( - key = args.key.toString(), - ) { - typingIndicatorViewModelFactory.create(args) - } - } + wireViewModelScoped< + TypingIndicatorViewModelImpl, + TypingIndicatorViewModel, + TypingIndicatorArgs, + TypingIndicatorViewModelFactory, + >(TypingIndicatorArgs(conversationId)) ) { UsersTypingIndicator(usersTyping = viewModel.state().usersTyping) } 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 2020b78ef5c..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.metro.metroViewModel +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,12 @@ fun MessageOptionsModalSheetLayout( onDownloadAssetClick: (messageId: String) -> Unit, onOpenAssetClick: (messageId: String) -> Unit, viewModel: MessageOptionsMenuViewModel = - MessageOptionsMenuArgs(conversationId).let { args -> - metroViewModel(key = args.key) { - messageOptionsMenuViewModelFactory.create(args) - } - } + wireViewModelScoped< + MessageOptionsMenuViewModelImpl, + MessageOptionsMenuViewModel, + MessageOptionsMenuArgs, + MessageOptionsMenuViewModelFactory, + >(MessageOptionsMenuArgs(conversationId)) ) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current 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 b2d25d9df70..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.metro.metroViewModel +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 @@ -67,6 +67,7 @@ import com.wire.android.ui.common.typography import com.wire.android.ui.home.conversations.LocalAssetLocalPathKeyInScopeResolver 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 @@ -622,9 +623,22 @@ private fun QuotedImageThumbnail( val keyInScopeResolver = LocalAssetLocalPathKeyInScopeResolver.current val viewModel: AssetLocalPathViewModel = if (keyInScopeResolver != null && keyInScopeResolver(args.key)) { - assetLocalPathViewModel(args) + wireViewModelScoped< + AssetLocalPathViewModelImpl, + AssetLocalPathViewModel, + AssetLocalPathArgs, + AssetLocalPathViewModelFactory, + >( + arguments = args, + keyInScopeResolver = keyInScopeResolver, + ) } else { - assetLocalPathViewModel(args) + wireViewModelScoped< + AssetLocalPathViewModelImpl, + AssetLocalPathViewModel, + AssetLocalPathArgs, + AssetLocalPathViewModelFactory, + >(args) } LaunchedEffect(Unit) { @@ -658,12 +672,6 @@ private fun QuotedImageThumbnail( } } -@Composable -private fun assetLocalPathViewModel(args: AssetLocalPathArgs): AssetLocalPathViewModel = - metroViewModel(key = args.key) { - assetLocalPathViewModelFactory.create(args) - } - @Composable fun QuotedAudioMessage( senderName: UIText, 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 70f7cbf2351..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.metro.metroViewModel +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 @@ -172,11 +173,12 @@ fun MessageButtonsContent( messageStyle: MessageStyle, modifier: Modifier = Modifier, viewModel: CompositeMessageViewModel = - CompositeMessageArgs(conversationId, messageId).let { args -> - metroViewModel(key = args.key) { - compositeMessageViewModelFactory.create(args) - } - } + wireViewModelScoped< + CompositeMessageViewModelImpl, + CompositeMessageViewModel, + CompositeMessageArgs, + 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 d4b1d046a26..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 @@ -60,16 +60,16 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource 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.metro.metroViewModel +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 @@ -90,6 +90,7 @@ import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.WireCircularProgressIndicator import com.wire.android.ui.common.spacers.HorizontalSpace +import com.wire.android.ui.home.conversations.LocalAudioMessageKeyInScopeResolver import com.wire.android.ui.home.conversations.messages.item.MessageStyle import com.wire.android.ui.home.conversations.messages.item.isBubble import com.wire.android.ui.home.conversations.messages.item.playedColor @@ -180,13 +181,21 @@ private fun UploadedAudioMessage( messageStyle: MessageStyle, modifier: Modifier = Modifier, ) { - val viewModel: AudioMessageViewModel = when { - LocalInspectionMode.current -> remember { object : AudioMessageViewModel {} } - else -> metroViewModel( - key = audioMessageArgs.key.toString(), - ) { - audioMessageViewModelFactory.create(audioMessageArgs) - } + val keyInScopeResolver = LocalAudioMessageKeyInScopeResolver.current + val viewModel: AudioMessageViewModel = if (keyInScopeResolver != null) { + wireViewModelScoped< + AudioMessageViewModelImpl, + AudioMessageViewModel, + AudioMessageArgs, + AudioMessageViewModelFactory + >(audioMessageArgs, keyInScopeResolver) + } else { + wireViewModelScoped< + AudioMessageViewModelImpl, + AudioMessageViewModel, + AudioMessageArgs, + AudioMessageViewModelFactory + >(audioMessageArgs) } val sanitizedAudioState by remember(viewModel.state.audioState, audioMessageDurationInMs) { 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 f15308666ec..cd7392013f3 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,7 +31,8 @@ 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.metro.metroViewModel +import com.wire.android.di.AssistedViewModelFactory +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 @@ -245,13 +246,14 @@ fun SelfDeletingMessageAction( conversationId: ConversationId, onButtonClicked: (SelfDeletionTimer) -> Unit, viewModel: SelfDeletingMessageActionViewModel = - SelfDeletingMessageActionArgs(conversationId = conversationId).let { args -> - metroViewModel( - key = args.key.toString(), - ) { - selfDeletingMessageActionViewModelFactory.create(args) - } - }, + wireViewModelScoped< + SelfDeletingMessageActionViewModelImpl, + SelfDeletingMessageActionViewModel, + SelfDeletingMessageActionArgs, + AssistedViewModelFactory + >( + SelfDeletingMessageActionArgs(conversationId = conversationId) + ), ) { when (val state = viewModel.state()) { SelfDeletionTimer.Disabled -> {} 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 9d137cea697..c18f2c2e8a5 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,8 @@ 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.metro.metroViewModel +import com.wire.android.di.AssistedViewModelFactory +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 @@ -184,11 +185,14 @@ private fun InputContent( onPlusClick: () -> Unit, modifier: Modifier = Modifier, viewModel: SelfDeletingMessageActionViewModel = - SelfDeletingMessageActionArgs(conversationId = conversationId).let { args -> - metroViewModel(key = args.key) { - selfDeletingMessageActionViewModelFactory.create(args) - } - }, + wireViewModelScoped< + SelfDeletingMessageActionViewModelImpl, + SelfDeletingMessageActionViewModel, + SelfDeletingMessageActionArgs, + AssistedViewModelFactory + >( + SelfDeletingMessageActionArgs(conversationId = conversationId) + ), ) { ConstraintLayout(modifier = modifier) { val (additionalOptionButton, input, actions) = createRefs() 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 93c9ee4391e..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.metro.metroViewModel +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,9 +52,7 @@ fun AdditionalOptionButton( onClick: () -> Unit, modifier: Modifier = Modifier, viewModel: IsFileSharingEnabledViewModel = - metroViewModel { - isFileSharingEnabledViewModelFactory.create() - } + wireViewModelScoped() ) { var enableAgain by remember { mutableStateOf(true) } LaunchedEffect(enableAgain, block = { 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 89c4db7e2d9..5ca039009d8 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.wire.android.di.metro.metroViewModel +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 @@ -51,7 +51,7 @@ fun RecordAudioComponent( onCloseRecordAudio: () -> Unit, modifier: Modifier = Modifier, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - viewModel: RecordAudioViewModel = metroViewModel { recordAudioViewModelFactory.create() } + viewModel: RecordAudioViewModel = wireViewModelScoped() ) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current 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 988dbe4aa43..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 @@ -37,7 +37,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import com.wire.android.R -import com.wire.android.di.metro.metroViewModel +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,9 +79,8 @@ fun JoinConversationViaDeepLinkDialog( onFlowCompleted: (conversationId: ConversationId?) -> Unit, modifier: Modifier = Modifier, ) { - val viewModel: JoinConversationViaCodeViewModel = metroViewModel { - joinConversationViaCodeViewModelFactory.create() - } + val viewModel: JoinConversationViaCodeViewModel = + wireViewModelScoped() val isLoading: Boolean by remember { derivedStateOf { viewModel.state is JoinViaDeepLinkDialogState.Loading } From 9efa3465c73a671d7df4086038c940c3bc600915 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Fri, 15 May 2026 10:21:56 +0200 Subject: [PATCH 37/46] feat: add ios shared pilot graph --- wire-ios-shared/build.gradle.kts | 32 +++++ .../com/wire/ios/shared/IosViewModel.kt | 42 +++++++ .../kotlin/com/wire/ios/shared/NoEffect.kt | 20 ++++ .../wire/ios/shared/WireIosSharedConfig.kt | 49 ++++++++ .../com/wire/ios/shared/WireIosSharedScope.kt | 20 ++++ .../ios/shared/auth/bridge/AuthIosBridge.kt | 53 +++++++++ .../shared/auth/email/LoginEmailContract.kt | 89 ++++++++++++++ .../shared/auth/login/model/LoginNavArgs.kt | 60 ++++++++++ .../login/model/LoginNavigationCommand.kt | 39 +++++++ .../auth/login/model/LoginPasswordPath.kt | 29 +++++ .../auth/login/model/LoginScreenState.kt | 43 +++++++ .../auth/login/model/LoginServerLinks.kt | 39 +++++++ .../newlogin/NewLoginIdentifierContract.kt | 110 ++++++++++++++++++ .../NewLoginIdentifierStateReducer.kt | 36 ++++++ .../ios/shared/auth/sso/LoginSsoContract.kt | 94 +++++++++++++++ .../ios/shared/auth/welcome/WelcomeEffect.kt | 33 ++++++ .../ios/shared/auth/welcome/WelcomeIntent.kt | 25 ++++ .../auth/welcome/WelcomeIosViewModel.kt | 110 ++++++++++++++++++ .../ios/shared/auth/welcome/WelcomeState.kt | 29 +++++ .../com/wire/ios/shared/IosViewModelTest.kt | 59 ++++++++++ .../shared/auth/bridge/AuthIosBridgeTest.kt | 95 +++++++++++++++ .../auth/email/LoginEmailContractTest.kt | 61 ++++++++++ .../auth/login/model/LoginNavArgsTest.kt | 43 +++++++ .../NewLoginIdentifierStateReducerTest.kt | 52 +++++++++ .../shared/auth/sso/LoginSsoContractTest.kt | 79 +++++++++++++ .../auth/welcome/WelcomeContractTest.kt | 79 +++++++++++++ .../welcome/WelcomeIosViewModelFactoryTest.kt | 84 +++++++++++++ .../ios/shared/export/WireIosSharedGraph.kt | 64 ++++++++++ 28 files changed, 1568 insertions(+) create mode 100644 wire-ios-shared/build.gradle.kts create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridge.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt create mode 100644 wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt create mode 100644 wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt create mode 100644 wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridgeTest.kt create mode 100644 wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt create mode 100644 wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt create mode 100644 wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt create mode 100644 wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt create mode 100644 wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt create mode 100644 wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt create mode 100644 wire-ios-shared/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt diff --git a/wire-ios-shared/build.gradle.kts b/wire-ios-shared/build.gradle.kts new file mode 100644 index 00000000000..b28a66a821b --- /dev/null +++ b/wire-ios-shared/build.gradle.kts @@ -0,0 +1,32 @@ +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(libs.coroutines.core) + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(libs.coroutines.test) + } + } + } + + targets.withType().configureEach { + binaries.framework { + baseName = "WireIosShared" + isStatic = false + } + } +} diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt new file mode 100644 index 00000000000..442a8010f3e --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.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.ios.shared + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** + * 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( + 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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt new file mode 100644 index 00000000000..5a4c2bb04f2 --- /dev/null +++ b/wire-ios-shared/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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt new file mode 100644 index 00000000000..5d6738105e6 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.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.ios.shared + +import com.wire.ios.shared.auth.login.model.LoginServerLinks + +data class WireIosSharedConfig( + val defaultServerLinks: LoginServerLinks, + val isThereActiveSession: Boolean = false, + val maxAccountsReached: Boolean = false, + val nomadAccountBlocksLogin: Boolean = false, + val isAccountCreationAllowed: Boolean = true, + val useNewRegistration: Boolean = true, +) + +fun createWireIosSharedConfig(defaultServerLinks: LoginServerLinks): WireIosSharedConfig = + WireIosSharedConfig(defaultServerLinks = defaultServerLinks) + +fun createWireIosSharedConfig( + defaultServerLinks: LoginServerLinks, + isThereActiveSession: Boolean, + maxAccountsReached: Boolean, + nomadAccountBlocksLogin: Boolean, + isAccountCreationAllowed: Boolean, + useNewRegistration: Boolean, +): WireIosSharedConfig = + WireIosSharedConfig( + defaultServerLinks = defaultServerLinks, + isThereActiveSession = isThereActiveSession, + maxAccountsReached = maxAccountsReached, + nomadAccountBlocksLogin = nomadAccountBlocksLogin, + isAccountCreationAllowed = isAccountCreationAllowed, + useNewRegistration = useNewRegistration, + ) diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt new file mode 100644 index 00000000000..171952c6074 --- /dev/null +++ b/wire-ios-shared/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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridge.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridge.kt new file mode 100644 index 00000000000..bf82a9c4054 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridge.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.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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt new file mode 100644 index 00000000000..009bbd43db5 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.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.ios.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(), +) + +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 +} + +sealed interface LoginEmailEffect { + data class LoginSucceeded( + val initialSyncCompleted: Boolean, + val isE2EIRequired: Boolean, + ) : LoginEmailEffect + data object RemoveDeviceNeeded : LoginEmailEffect +} diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt new file mode 100644 index 00000000000..3d36acb40f5 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/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.ios.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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt new file mode 100644 index 00000000000..a06cbdd8dd8 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/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.ios.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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt new file mode 100644 index 00000000000..cec8b633739 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/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.ios.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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt new file mode 100644 index 00000000000..0248502b2a0 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/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.ios.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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt new file mode 100644 index 00000000000..356065d6817 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/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.ios.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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt new file mode 100644 index 00000000000..1d2e0533cca --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt @@ -0,0 +1,110 @@ +/* + * 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.auth.login.model.LoginServerLinks + +/** + * Swift-facing state for the first new-login screen where the user enters an email or SSO code. + * + * This mirrors the Android new-login state without exposing Android, Compose navigation, + * SavedStateHandle, or Kalium-specific error types. + */ +data class NewLoginIdentifierState( + val userIdentifier: String = "", + val isThereActiveSession: Boolean = false, + val userIdentifierEnabled: Boolean = true, + val nextEnabled: Boolean = false, + val flowState: NewLoginIdentifierFlowState = NewLoginIdentifierFlowState.Default, +) + +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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt new file mode 100644 index 00000000000..e5d53f67792 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/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.ios.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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt new file mode 100644 index 00000000000..9cf5e7a05d5 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.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.ios.shared.auth.sso + +import com.wire.ios.shared.auth.login.model.LoginServerLinks + +/** + * Swift-facing state for the SSO login screen. + * + * This mirrors the Android SSO screen state without exposing Compose, Android, or Kalium UI types. + */ +data class LoginSsoState( + val ssoCode: String = "", + val loginEnabled: Boolean = false, + val flowState: LoginSsoFlowState = LoginSsoFlowState.Default, + val customServerDialogState: LoginSsoCustomServerDialogState? = null, +) + +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] using an iOS gateway 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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt new file mode 100644 index 00000000000..b29ebb5504b --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/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.ios.shared.auth.welcome + +import com.wire.ios.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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt new file mode 100644 index 00000000000..498a9941c66 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/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.ios.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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt new file mode 100644 index 00000000000..b9728758435 --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt @@ -0,0 +1,110 @@ +/* + * 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.IosViewModel +import com.wire.ios.shared.WireIosSharedConfig +import com.wire.ios.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 + +class WelcomeIosViewModel( + private val delegate: IosViewModel, +) { + val state = delegate.state + val effects = delegate.effects + + fun sendIntent(intent: WelcomeIntent) { + delegate.sendIntent(intent) + } + + fun close() { + delegate.close() + } +} + +@Inject +class WelcomeIosViewModelFactory( + private val config: WireIosSharedConfig, +) { + fun create(): WelcomeIosViewModel = + WelcomeIosViewModel(createGeneric()) + + fun createGeneric(): IosViewModel { + 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 IosViewModel( + 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) + } +} + +fun createWelcomeIosViewModel( + welcomeIosViewModelFactory: WelcomeIosViewModelFactory, +): WelcomeIosViewModel = + welcomeIosViewModelFactory.create() + +fun createGenericWelcomeIosViewModel( + welcomeIosViewModelFactory: WelcomeIosViewModelFactory, +): IosViewModel = + welcomeIosViewModelFactory.createGeneric() diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt new file mode 100644 index 00000000000..c10119a01bd --- /dev/null +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/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.ios.shared.auth.welcome + +import com.wire.ios.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/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt new file mode 100644 index 00000000000..6b75b75445b --- /dev/null +++ b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.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.ios.shared + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow + +class IosViewModelTest { + + @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 givenCloseHandler_whenClose_thenDelegatesToHandler() { + var closed = false + val viewModel = IosViewModel( + state = MutableStateFlow(TestState), + effects = MutableSharedFlow(), + onIntent = {}, + onClose = { closed = true }, + ) + + viewModel.close() + + assertTrue(closed) + } + + private data object TestState + private data object TestIntent +} diff --git a/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridgeTest.kt b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridgeTest.kt new file mode 100644 index 00000000000..ac02ae7076f --- /dev/null +++ b/wire-ios-shared/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/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt new file mode 100644 index 00000000000..2a18423a32d --- /dev/null +++ b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt @@ -0,0 +1,61 @@ +/* + * 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 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) + assertEquals("", state.proxyIdentifier) + assertEquals("", state.proxyPassword) + assertTrue(state.userIdentifierEnabled) + assertFalse(state.loginEnabled) + assertIs(state.flowState) + assertEquals(LoginEmailVerificationCodeState.DEFAULT_VERIFICATION_CODE_LENGTH, state.secondFactorVerificationCode.codeLength) + assertFalse(state.secondFactorVerificationCode.isCodeInputNecessary) + assertFalse(state.secondFactorVerificationCode.isCurrentCodeInvalid) + } + + @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/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt new file mode 100644 index 00000000000..0fcae244a45 --- /dev/null +++ b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/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.ios.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/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt new file mode 100644 index 00000000000..6b066cd14c5 --- /dev/null +++ b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.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.ios.shared.auth.newlogin + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +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) + } + + @Test + fun givenTextFieldError_whenUserIdentifierChanges_thenErrorIsCleared() { + val state = NewLoginIdentifierState( + flowState = NewLoginIdentifierFlowState.TextFieldError(NewLoginIdentifierTextFieldError.InvalidValue), + ).withUserIdentifier("user@example.com") + + assertEquals(NewLoginIdentifierFlowState.Default, state.flowState) + } + + @Test + fun givenUserIdentifier_whenLoadingStarts_thenNextIsDisabled() { + val state = NewLoginIdentifierState(userIdentifier = "user@example.com") + .withFlowState(NewLoginIdentifierFlowState.Loading) + + assertFalse(state.nextEnabled) + } +} diff --git a/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt new file mode 100644 index 00000000000..fdcf84f0959 --- /dev/null +++ b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/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.ios.shared.auth.sso + +import com.wire.ios.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_thenItCarriesOnlySwiftFacingData() { + 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/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt new file mode 100644 index 00000000000..eecae40c7a2 --- /dev/null +++ b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/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.ios.shared.auth.welcome + +import com.wire.ios.shared.auth.login.model.LoginApiProxy +import com.wire.ios.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/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt new file mode 100644 index 00000000000..a77f1257785 --- /dev/null +++ b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.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.ios.shared.auth.welcome + +import com.wire.ios.shared.WireIosSharedConfig +import com.wire.ios.shared.auth.login.model.LoginApiProxy +import com.wire.ios.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 WelcomeIosViewModelFactoryTest { + @Test + fun givenConfig_whenCreatingViewModel_thenStateUsesConfig() { + val viewModel = WelcomeIosViewModelFactory( + config = WireIosSharedConfig( + 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 = WelcomeIosViewModelFactory(WireIosSharedConfig(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 = WelcomeIosViewModelFactory(WireIosSharedConfig(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/wire-ios-shared/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt b/wire-ios-shared/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt new file mode 100644 index 00000000000..0ebf3b109f9 --- /dev/null +++ b/wire-ios-shared/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.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.ios.shared.export + +import com.wire.ios.shared.IosViewModel +import com.wire.ios.shared.WireIosSharedConfig +import com.wire.ios.shared.WireIosSharedScope +import com.wire.ios.shared.auth.login.model.LoginServerLinks +import com.wire.ios.shared.auth.welcome.WelcomeEffect +import com.wire.ios.shared.auth.welcome.WelcomeIntent +import com.wire.ios.shared.auth.welcome.WelcomeIosViewModel +import com.wire.ios.shared.auth.welcome.WelcomeIosViewModelFactory +import com.wire.ios.shared.auth.welcome.WelcomeState +import com.wire.ios.shared.auth.welcome.createGenericWelcomeIosViewModel +import com.wire.ios.shared.auth.welcome.createWelcomeIosViewModel +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.createGraphFactory + +@DependencyGraph(WireIosSharedScope::class) +interface WireIosSharedGraph { + @DependencyGraph.Factory + fun interface Factory { + fun create(@Provides config: WireIosSharedConfig): WireIosSharedGraph + } + + @Provides + fun provideWelcomeIosViewModel( + welcomeIosViewModelFactory: WelcomeIosViewModelFactory, + ): WelcomeIosViewModel = + createWelcomeIosViewModel(welcomeIosViewModelFactory) + + @Provides + fun provideGenericWelcomeIosViewModel( + welcomeIosViewModelFactory: WelcomeIosViewModelFactory, + ): IosViewModel = + createGenericWelcomeIosViewModel(welcomeIosViewModelFactory) + + val welcomeViewModel: WelcomeIosViewModel + val welcomeIosViewModel: IosViewModel +} + +fun createWireIosSharedGraph(config: WireIosSharedConfig): WireIosSharedGraph = + createGraphFactory().create(config) + +fun createWireIosShared(defaultServerLinks: LoginServerLinks): WireIosSharedGraph = + createWireIosSharedGraph( + config = com.wire.ios.shared.createWireIosSharedConfig(defaultServerLinks) + ) From b231763d098851b16abdd6b5643dbb68e239df96 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Fri, 15 May 2026 11:00:13 +0200 Subject: [PATCH 38/46] feat: add swift friendly ios view model observers --- .../com/wire/ios/shared/IosViewModel.kt | 126 ++++++++++++++++++ .../auth/welcome/WelcomeIosViewModel.kt | 17 ++- .../com/wire/ios/shared/IosViewModelTest.kt | 60 ++++++++- 3 files changed, 194 insertions(+), 9 deletions(-) diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt index 442a8010f3e..646e8826e20 100644 --- a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt @@ -17,8 +17,24 @@ */ 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. @@ -27,16 +43,126 @@ import kotlinx.coroutines.flow.StateFlow * 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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt index b9728758435..74a87cf99fe 100644 --- a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt +++ b/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt @@ -18,6 +18,8 @@ package com.wire.ios.shared.auth.welcome import com.wire.ios.shared.IosViewModel +import com.wire.ios.shared.IosCloseable +import com.wire.ios.shared.IosObservableViewModel import com.wire.ios.shared.WireIosSharedConfig import com.wire.ios.shared.auth.login.model.LoginServerLinks import dev.zacsweers.metro.Inject @@ -28,15 +30,24 @@ import kotlinx.coroutines.flow.asStateFlow class WelcomeIosViewModel( private val delegate: IosViewModel, -) { +) : IosObservableViewModel { val state = delegate.state val effects = delegate.effects - fun sendIntent(intent: WelcomeIntent) { + 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) } - fun close() { + override fun close() { delegate.close() } } diff --git a/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt index 6b75b75445b..735ad37016e 100644 --- a/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt +++ b/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt @@ -17,13 +17,25 @@ */ package com.wire.ios.shared -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue +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() { @@ -39,19 +51,55 @@ class IosViewModelTest { 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 closed = false + var closeCount = 0 val viewModel = IosViewModel( state = MutableStateFlow(TestState), effects = MutableSharedFlow(), onIntent = {}, - onClose = { closed = true }, + onClose = { closeCount++ }, ) + viewModel.close() viewModel.close() - assertTrue(closed) + assertEquals(1, closeCount) } private data object TestState From fd4625a73261922283038029e7237b1b49cdbfc9 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Fri, 15 May 2026 11:16:23 +0200 Subject: [PATCH 39/46] feat: add shared ios login identifier view model --- settings.gradle.kts | 2 +- .../export-ios}/build.gradle.kts | 0 .../com/wire/ios/shared/IosViewModel.kt | 0 .../kotlin/com/wire/ios/shared/NoEffect.kt | 0 .../wire/ios/shared/WireIosSharedConfig.kt | 0 .../com/wire/ios/shared/WireIosSharedScope.kt | 0 .../ios/shared/auth/bridge/AuthIosBridge.kt | 0 .../shared/auth/email/LoginEmailContract.kt | 0 .../shared/auth/login/model/LoginNavArgs.kt | 0 .../login/model/LoginNavigationCommand.kt | 0 .../auth/login/model/LoginPasswordPath.kt | 0 .../auth/login/model/LoginScreenState.kt | 0 .../auth/login/model/LoginServerLinks.kt | 0 .../newlogin/NewLoginIdentifierContract.kt | 0 .../NewLoginIdentifierIosViewModel.kt | 193 ++++++++++++++++++ .../NewLoginIdentifierStateReducer.kt | 0 .../ios/shared/auth/sso/LoginSsoContract.kt | 0 .../ios/shared/auth/welcome/WelcomeEffect.kt | 0 .../ios/shared/auth/welcome/WelcomeIntent.kt | 0 .../auth/welcome/WelcomeIosViewModel.kt | 0 .../ios/shared/auth/welcome/WelcomeState.kt | 0 .../com/wire/ios/shared/IosViewModelTest.kt | 0 .../shared/auth/bridge/AuthIosBridgeTest.kt | 0 .../auth/email/LoginEmailContractTest.kt | 0 .../auth/login/model/LoginNavArgsTest.kt | 0 ...wLoginIdentifierIosViewModelFactoryTest.kt | 95 +++++++++ .../NewLoginIdentifierStateReducerTest.kt | 0 .../shared/auth/sso/LoginSsoContractTest.kt | 0 .../auth/welcome/WelcomeContractTest.kt | 0 .../welcome/WelcomeIosViewModelFactoryTest.kt | 0 .../ios/shared/export/WireIosSharedGraph.kt | 21 ++ 31 files changed, 310 insertions(+), 1 deletion(-) rename {wire-ios-shared => shared/export-ios}/build.gradle.kts (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridge.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt (100%) create mode 100644 shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModel.kt rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonTest/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridgeTest.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt (100%) create mode 100644 shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt rename {wire-ios-shared => shared/export-ios}/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt (100%) rename {wire-ios-shared => shared/export-ios}/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt (66%) 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/wire-ios-shared/build.gradle.kts b/shared/export-ios/build.gradle.kts similarity index 100% rename from wire-ios-shared/build.gradle.kts rename to shared/export-ios/build.gradle.kts diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/IosViewModel.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/NoEffect.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedConfig.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/WireIosSharedScope.kt diff --git a/wire-ios-shared/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 similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridge.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridge.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt 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..344de95eefe --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModel.kt @@ -0,0 +1,193 @@ +/* + * 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.ios.shared.WireIosSharedConfig +import com.wire.ios.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 +import kotlinx.coroutines.flow.update + +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 config: WireIosSharedConfig, +) { + fun create(): NewLoginIdentifierIosViewModel = + NewLoginIdentifierIosViewModel(createGeneric()) + + fun createGeneric(): IosViewModel { + val state = MutableStateFlow( + NewLoginIdentifierState( + isThereActiveSession = config.isThereActiveSession, + ) + ) + val effects = MutableSharedFlow(extraBufferCapacity = 1) + + return IosViewModel( + state = state.asStateFlow(), + effects = effects.asSharedFlow(), + onIntent = { intent -> + when (intent) { + is NewLoginIdentifierIntent.UserIdentifierChanged -> { + state.update { it.withUserIdentifier(intent.userIdentifier) } + } + + NewLoginIdentifierIntent.Submit -> { + submit( + state = state, + effects = effects, + serverLinks = config.defaultServerLinks, + ) + } + + 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( + result = intent.result, + state = state, + effects = effects, + ) + } + } + } + ) + } + + private fun submit( + state: MutableStateFlow, + effects: MutableSharedFlow, + serverLinks: LoginServerLinks, + ) { + val userIdentifier = state.value.userIdentifier.trim() + when { + userIdentifier.isValidSsoCode() -> { + effects.tryEmit( + NewLoginIdentifierEffect.OpenSSO( + url = serverLinks.accounts, + config = NewLoginSsoUrlConfig(userIdentifier = userIdentifier), + ) + ) + state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } + } + + userIdentifier.isValidEmail() -> { + effects.tryEmit( + NewLoginIdentifierEffect.OpenEmailPassword( + userIdentifier = userIdentifier, + path = NewLoginPasswordPath(), + ) + ) + state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } + } + + else -> state.update { + it.withFlowState( + NewLoginIdentifierFlowState.TextFieldError(NewLoginIdentifierTextFieldError.InvalidValue) + ) + } + } + } + + 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) + ) + ) + } + } + } + } +} + +fun createNewLoginIdentifierIosViewModel( + factory: NewLoginIdentifierIosViewModelFactory, +): NewLoginIdentifierIosViewModel = + factory.create() + +fun createGenericNewLoginIdentifierIosViewModel( + factory: NewLoginIdentifierIosViewModelFactory, +): IosViewModel = + factory.createGeneric() + +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/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt diff --git a/wire-ios-shared/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 similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModel.kt diff --git a/wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt similarity index 100% rename from wire-ios-shared/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt rename to shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt diff --git a/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt similarity index 100% rename from wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt rename to shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/IosViewModelTest.kt diff --git a/wire-ios-shared/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 similarity index 100% rename from wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridgeTest.kt rename to shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/bridge/AuthIosBridgeTest.kt diff --git a/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt similarity index 100% rename from wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt rename to shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt diff --git a/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt similarity index 100% rename from wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt rename to shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt diff --git a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt new file mode 100644 index 00000000000..c75295a23b7 --- /dev/null +++ b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.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.newlogin + +import com.wire.ios.shared.WireIosSharedConfig +import com.wire.ios.shared.auth.login.model.LoginServerLinks +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class NewLoginIdentifierIosViewModelFactoryTest { + @Test + fun givenIdentifierChanged_whenSendingIntent_thenStateIsUpdated() { + val viewModel = NewLoginIdentifierIosViewModelFactory(WireIosSharedConfig(serverLinks)).create() + + 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 = NewLoginIdentifierIosViewModelFactory(WireIosSharedConfig(serverLinks)).create() + 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 = NewLoginIdentifierIosViewModelFactory(WireIosSharedConfig(serverLinks)).create() + viewModel.sendIntent(NewLoginIdentifierIntent.UserIdentifierChanged("user@example.com")) + val effect = async(start = CoroutineStart.UNDISPATCHED) { + var observedEffect: NewLoginIdentifierEffect? = null + val closeable = viewModel.observeEffect { observedEffect = it } + viewModel.sendIntent(NewLoginIdentifierIntent.Submit) + closeable.close() + observedEffect + } + + val openEmailPassword = assertIs(effect.await()) + assertEquals("user@example.com", openEmailPassword.userIdentifier) + } + + @Test + fun givenSsoFailure_whenReceived_thenDialogErrorIsShown() { + val viewModel = NewLoginIdentifierIosViewModelFactory(WireIosSharedConfig(serverLinks)).create() + + viewModel.sendIntent(NewLoginIdentifierIntent.SSOResultReceived(NewLoginSsoResult.Failure(NewLoginSsoFailureCode.InvalidCode))) + + assertEquals( + NewLoginIdentifierFlowState.DialogError( + NewLoginIdentifierDialogError.SSOResultFailure(NewLoginSsoFailureCode.InvalidCode) + ), + viewModel.currentState.flowState, + ) + } + + 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/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt similarity index 100% rename from wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt rename to shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt diff --git a/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt similarity index 100% rename from wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt rename to shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt diff --git a/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt similarity index 100% rename from wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt rename to shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt diff --git a/wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt similarity index 100% rename from wire-ios-shared/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt rename to shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt diff --git a/wire-ios-shared/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt similarity index 66% rename from wire-ios-shared/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt rename to shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt index 0ebf3b109f9..e47af191534 100644 --- a/wire-ios-shared/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt +++ b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/export/WireIosSharedGraph.kt @@ -21,6 +21,13 @@ import com.wire.ios.shared.IosViewModel import com.wire.ios.shared.WireIosSharedConfig import com.wire.ios.shared.WireIosSharedScope import com.wire.ios.shared.auth.login.model.LoginServerLinks +import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierEffect +import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierIntent +import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierIosViewModel +import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierIosViewModelFactory +import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierState +import com.wire.ios.shared.auth.newlogin.createGenericNewLoginIdentifierIosViewModel +import com.wire.ios.shared.auth.newlogin.createNewLoginIdentifierIosViewModel import com.wire.ios.shared.auth.welcome.WelcomeEffect import com.wire.ios.shared.auth.welcome.WelcomeIntent import com.wire.ios.shared.auth.welcome.WelcomeIosViewModel @@ -51,8 +58,22 @@ interface WireIosSharedGraph { ): IosViewModel = createGenericWelcomeIosViewModel(welcomeIosViewModelFactory) + @Provides + fun provideNewLoginIdentifierViewModel( + newLoginIdentifierIosViewModelFactory: NewLoginIdentifierIosViewModelFactory, + ): NewLoginIdentifierIosViewModel = + createNewLoginIdentifierIosViewModel(newLoginIdentifierIosViewModelFactory) + + @Provides + fun provideGenericNewLoginIdentifierIosViewModel( + newLoginIdentifierIosViewModelFactory: NewLoginIdentifierIosViewModelFactory, + ): IosViewModel = + createGenericNewLoginIdentifierIosViewModel(newLoginIdentifierIosViewModelFactory) + val welcomeViewModel: WelcomeIosViewModel val welcomeIosViewModel: IosViewModel + val newLoginIdentifierViewModel: NewLoginIdentifierIosViewModel + val newLoginIdentifierIosViewModel: IosViewModel } fun createWireIosSharedGraph(config: WireIosSharedConfig): WireIosSharedGraph = From d149752600eb58605d4eb8cc5bf528f5be0cb327 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Fri, 15 May 2026 13:10:29 +0200 Subject: [PATCH 40/46] fix: use concrete composer scoped view model factory --- .../android/ui/home/messagecomposer/MessageComposeActions.kt | 4 ++-- .../android/ui/home/messagecomposer/MessageComposerInput.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 cd7392013f3..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,7 +31,6 @@ 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.AssistedViewModelFactory import com.wire.android.di.wireViewModelScoped import com.wire.android.model.ClickBlockParams import com.wire.android.ui.common.button.WireButtonState @@ -39,6 +38,7 @@ 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 @@ -250,7 +250,7 @@ fun SelfDeletingMessageAction( SelfDeletingMessageActionViewModelImpl, SelfDeletingMessageActionViewModel, SelfDeletingMessageActionArgs, - AssistedViewModelFactory + 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 c18f2c2e8a5..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,6 @@ 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.AssistedViewModelFactory import com.wire.android.di.wireViewModelScoped import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions @@ -68,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 @@ -189,7 +189,7 @@ private fun InputContent( SelfDeletingMessageActionViewModelImpl, SelfDeletingMessageActionViewModel, SelfDeletingMessageActionArgs, - AssistedViewModelFactory + SelfDeletingMessageActionViewModelFactory >( SelfDeletingMessageActionArgs(conversationId = conversationId) ), From 80dd1f3d5160e65336b06168ce18c3ca2c85c9e5 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Fri, 15 May 2026 13:30:28 +0200 Subject: [PATCH 41/46] feat: add Kalium-backed iOS login probe --- kalium | 2 +- shared/export-ios/build.gradle.kts | 1 + .../wire/ios/shared/WireIosSharedConfig.kt | 67 +++++ .../shared/auth/email/LoginEmailBackend.kt | 60 ++++ .../shared/auth/email/LoginEmailContract.kt | 6 + .../auth/email/LoginEmailIosViewModel.kt | 264 ++++++++++++++++++ .../newlogin/NewLoginIdentifierBackend.kt | 73 +++++ .../NewLoginIdentifierIosViewModel.kt | 92 ++++-- .../LoginEmailIosViewModelFactoryTest.kt | 119 ++++++++ ...wLoginIdentifierIosViewModelFactoryTest.kt | 23 +- .../auth/email/KaliumLoginEmailBackend.kt | 213 ++++++++++++++ .../model/LoginServerLinksKaliumMapper.kt | 58 ++++ .../KaliumNewLoginIdentifierBackend.kt | 176 ++++++++++++ .../ios/shared/export/WireIosSharedGraph.kt | 78 ++++++ 14 files changed, 1206 insertions(+), 26 deletions(-) create mode 100644 shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailBackend.kt create mode 100644 shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModel.kt create mode 100644 shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierBackend.kt create mode 100644 shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModelFactoryTest.kt create mode 100644 shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailBackend.kt create mode 100644 shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinksKaliumMapper.kt create mode 100644 shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/newlogin/KaliumNewLoginIdentifierBackend.kt 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/shared/export-ios/build.gradle.kts b/shared/export-ios/build.gradle.kts index b28a66a821b..2b19acd91f9 100644 --- a/shared/export-ios/build.gradle.kts +++ b/shared/export-ios/build.gradle.kts @@ -12,6 +12,7 @@ kotlin { val commonMain by getting { dependencies { api(libs.coroutines.core) + implementation("com.wire.kalium:kalium-logic") } } 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 index 5d6738105e6..c0e66c8bad7 100644 --- 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 @@ -21,6 +21,7 @@ import com.wire.ios.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, @@ -28,11 +29,38 @@ data class WireIosSharedConfig( 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, + ) + +fun createWireIosSharedConfig( + defaultServerLinks: LoginServerLinks, + runtimeConfig: IosKaliumRuntimeConfig?, isThereActiveSession: Boolean, maxAccountsReached: Boolean, nomadAccountBlocksLogin: Boolean, @@ -41,9 +69,48 @@ fun createWireIosSharedConfig( ): 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, + ) + +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/auth/email/LoginEmailBackend.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailBackend.kt new file mode 100644 index 00000000000..a9227fa047d --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailBackend.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.ios.shared.auth.email + +interface LoginEmailBackend { + suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailBackendResult + + suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult +} + +sealed interface LoginEmailBackendResult { + data class Success( + val initialSyncCompleted: Boolean, + val isE2EIRequired: Boolean, + ) : LoginEmailBackendResult + + data class SecondFactorRequired( + val email: String, + val isCurrentCodeInvalid: Boolean = false, + ) : LoginEmailBackendResult + + data object RemoveDeviceNeeded : LoginEmailBackendResult + data class Failure(val error: LoginEmailError) : LoginEmailBackendResult +} + +class LocalLoginEmailBackend : LoginEmailBackend { + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailBackendResult = + LoginEmailBackendResult.Success( + initialSyncCompleted = false, + isE2EIRequired = false, + ) + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult = + LoginEmailBackendResult.SecondFactorRequired(email = userIdentifier) +} diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt index 009bbd43db5..7d9730282fe 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt @@ -80,6 +80,12 @@ sealed interface LoginEmailIntent { data object ResendSecondFactorCode : LoginEmailIntent } +/** + * One-shot login actions for Swift 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, 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..e5a65f279dd --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModel.kt @@ -0,0 +1,264 @@ +/* + * 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 dev.zacsweers.metro.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +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 + +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 backend: LoginEmailBackend, +) { + fun create(userIdentifier: String = ""): LoginEmailIosViewModel = + LoginEmailIosViewModel(createGeneric(userIdentifier)) + + fun createGeneric(userIdentifier: String = ""): IosViewModel { + val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + val state = MutableStateFlow( + LoginEmailState( + userIdentifier = userIdentifier, + userIdentifierEnabled = userIdentifier.isBlank(), + loginEnabled = false, + ) + ) + val effects = MutableSharedFlow(extraBufferCapacity = 1) + + return IosViewModel( + state = state.asStateFlow(), + effects = effects.asSharedFlow(), + onIntent = { intent -> + when (intent) { + is LoginEmailIntent.UserIdentifierChanged -> + state.update { + it.copy( + userIdentifier = intent.value, + loginEnabled = it.canSubmit(userIdentifier = intent.value), + flowState = LoginEmailFlowState.Default, + ) + } + + is LoginEmailIntent.PasswordChanged -> + state.update { + it.copy( + password = intent.value, + loginEnabled = it.canSubmit(password = intent.value), + flowState = LoginEmailFlowState.Default, + ) + } + + is LoginEmailIntent.ProxyIdentifierChanged -> + state.update { it.copy(proxyIdentifier = intent.value) } + + is LoginEmailIntent.ProxyPasswordChanged -> + state.update { it.copy(proxyPassword = intent.value) } + + is LoginEmailIntent.SecondFactorCodeChanged -> + state.update { + it.copy( + secondFactorVerificationCode = it.secondFactorVerificationCode.copy( + code = intent.value, + isCurrentCodeInvalid = false, + ) + ) + } + + 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 -> + state.update { + it.copy( + secondFactorVerificationCode = LoginEmailVerificationCodeState(), + flowState = LoginEmailFlowState.Default, + ) + } + + LoginEmailIntent.ResendSecondFactorCode -> + requestSecondFactorCode(state, scope) + } + }, + onClose = { scope.close() }, + ) + } + + 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 { + when ( + val result = backend.login( + userIdentifier = currentState.userIdentifier, + password = currentState.password, + secondFactorVerificationCode = currentState.secondFactorVerificationCode.code.takeIf { it.isNotBlank() }, + usernameAllowed = usernameAllowed, + ) + ) { + is LoginEmailBackendResult.Failure -> { + state.update { + val flowState = LoginEmailFlowState.Error(result.error) + it.copy( + flowState = flowState, + loginEnabled = it.canSubmit(flowState = flowState), + ) + } + } + + LoginEmailBackendResult.RemoveDeviceNeeded -> { + effects.tryEmit(LoginEmailEffect.RemoveDeviceNeeded) + state.update { + val flowState = LoginEmailFlowState.Error(LoginEmailError.TooManyDevices) + it.copy( + flowState = flowState, + loginEnabled = it.canSubmit(flowState = flowState), + ) + } + } + + is LoginEmailBackendResult.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, + ), + ) + } + } + + is LoginEmailBackendResult.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 = backend.requestSecondFactorCode(state.value.userIdentifier)) { + is LoginEmailBackendResult.Failure -> + state.update { it.copy(flowState = LoginEmailFlowState.Error(result.error)) } + + is LoginEmailBackendResult.SecondFactorRequired -> + state.update { + it.copy( + flowState = LoginEmailFlowState.Default, + secondFactorVerificationCode = it.secondFactorVerificationCode.copy( + isCodeInputNecessary = true, + emailUsed = result.email, + isCurrentCodeInvalid = result.isCurrentCodeInvalid, + ), + ) + } + + LoginEmailBackendResult.RemoveDeviceNeeded, + is LoginEmailBackendResult.Success -> + Unit + } + } + } +} + +fun createLoginEmailIosViewModel( + factory: LoginEmailIosViewModelFactory, + userIdentifier: String = "", +): LoginEmailIosViewModel = + factory.create(userIdentifier) + +fun createGenericLoginEmailIosViewModel( + factory: LoginEmailIosViewModelFactory, + userIdentifier: String = "", +): IosViewModel = + factory.createGeneric(userIdentifier) + +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 + +private fun CoroutineScope.close() { + coroutineContext[Job]?.cancel() +} diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierBackend.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierBackend.kt new file mode 100644 index 00000000000..21e7df45d08 --- /dev/null +++ b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierBackend.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.ios.shared.auth.newlogin + +import com.wire.ios.shared.auth.login.model.LoginServerLinks + +/** + * Pilot adapter for the export-ios login probe. + * + * This keeps the current POC easy to exercise from Swift while the real migration shape is + * validated. It is not the target pattern for migrating Android ViewModels; shared ViewModels + * should eventually depend on Kalium use cases/facades directly through Metro, with only + * platform-specific capabilities extracted behind small gateways. + */ +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/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 index 344de95eefe..9aa53bf3ef3 100644 --- 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 @@ -21,13 +21,17 @@ import com.wire.ios.shared.IosCloseable import com.wire.ios.shared.IosObservableViewModel import com.wire.ios.shared.IosViewModel import com.wire.ios.shared.WireIosSharedConfig -import com.wire.ios.shared.auth.login.model.LoginServerLinks import dev.zacsweers.metro.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +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 class NewLoginIdentifierIosViewModel( private val delegate: IosViewModel, @@ -56,11 +60,13 @@ class NewLoginIdentifierIosViewModel( @Inject class NewLoginIdentifierIosViewModelFactory( private val config: WireIosSharedConfig, + private val backend: NewLoginIdentifierBackend, ) { fun create(): NewLoginIdentifierIosViewModel = NewLoginIdentifierIosViewModel(createGeneric()) fun createGeneric(): IosViewModel { + val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) val state = MutableStateFlow( NewLoginIdentifierState( isThereActiveSession = config.isThereActiveSession, @@ -81,7 +87,7 @@ class NewLoginIdentifierIosViewModelFactory( submit( state = state, effects = effects, - serverLinks = config.defaultServerLinks, + scope = scope, ) } @@ -107,35 +113,34 @@ class NewLoginIdentifierIosViewModelFactory( ) } } - } + }, + onClose = { scope.close() }, ) } private fun submit( state: MutableStateFlow, effects: MutableSharedFlow, - serverLinks: LoginServerLinks, + scope: CoroutineScope, ) { val userIdentifier = state.value.userIdentifier.trim() when { userIdentifier.isValidSsoCode() -> { - effects.tryEmit( - NewLoginIdentifierEffect.OpenSSO( - url = serverLinks.accounts, - config = NewLoginSsoUrlConfig(userIdentifier = userIdentifier), - ) + resolveWithBackend( + state = state, + effects = effects, + scope = scope, + resolve = { backend.initiateSso(userIdentifier) }, ) - state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } } userIdentifier.isValidEmail() -> { - effects.tryEmit( - NewLoginIdentifierEffect.OpenEmailPassword( - userIdentifier = userIdentifier, - path = NewLoginPasswordPath(), - ) + resolveWithBackend( + state = state, + effects = effects, + scope = scope, + resolve = { backend.resolveEmail(userIdentifier) }, ) - state.update { it.withFlowState(NewLoginIdentifierFlowState.Default) } } else -> state.update { @@ -146,6 +151,57 @@ class NewLoginIdentifierIosViewModelFactory( } } + 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, @@ -170,6 +226,10 @@ class NewLoginIdentifierIosViewModelFactory( } } +private fun CoroutineScope.close() { + coroutineContext[Job]?.cancel() +} + fun createNewLoginIdentifierIosViewModel( factory: NewLoginIdentifierIosViewModelFactory, ): NewLoginIdentifierIosViewModel = diff --git a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModelFactoryTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModelFactoryTest.kt new file mode 100644 index 00000000000..0a635c6ea1d --- /dev/null +++ b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModelFactoryTest.kt @@ -0,0 +1,119 @@ +/* + * 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 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 + +class LoginEmailIosViewModelFactoryTest { + @Test + fun givenCredentials_whenSubmitting_thenLoginSucceededEffectIsEmitted() = runTest { + val viewModel = LoginEmailIosViewModelFactory(SuccessBackend).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()) + + assertIs(effect.await()) + assertEquals(LoginEmailFlowState.Success(initialSyncCompleted = false, isE2EIRequired = false), viewModel.currentState.flowState) + } + + @Test + fun givenSecondFactorRequired_whenSubmitting_thenVerificationCodeStateIsShown() = runTest { + val viewModel = LoginEmailIosViewModelFactory(SecondFactorBackend).create() + viewModel.sendIntent(LoginEmailIntent.UserIdentifierChanged("user@example.com")) + viewModel.sendIntent(LoginEmailIntent.PasswordChanged("password")) + + viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) + + assertEquals(true, viewModel.currentState.secondFactorVerificationCode.isCodeInputNecessary) + assertEquals("user@example.com", viewModel.currentState.secondFactorVerificationCode.emailUsed) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun givenInvalidCredentials_whenSubmitting_thenErrorStateIsShownWithoutEffect() = runTest { + val viewModel = LoginEmailIosViewModelFactory(InvalidCredentialsBackend).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 SuccessBackend : LoginEmailBackend { + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailBackendResult = + LoginEmailBackendResult.Success(initialSyncCompleted = false, isE2EIRequired = false) + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult = + LoginEmailBackendResult.SecondFactorRequired(userIdentifier) + } + + private object SecondFactorBackend : LoginEmailBackend { + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailBackendResult = + LoginEmailBackendResult.SecondFactorRequired(userIdentifier) + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult = + LoginEmailBackendResult.SecondFactorRequired(userIdentifier) + } + + private object InvalidCredentialsBackend : LoginEmailBackend { + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailBackendResult = + LoginEmailBackendResult.Failure(LoginEmailError.InvalidCredentials) + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult = + LoginEmailBackendResult.SecondFactorRequired(userIdentifier) + } +} diff --git a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt index c75295a23b7..aa864500b45 100644 --- a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt +++ b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt @@ -21,6 +21,7 @@ import com.wire.ios.shared.WireIosSharedConfig import com.wire.ios.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 @@ -29,7 +30,7 @@ import kotlin.test.assertIs class NewLoginIdentifierIosViewModelFactoryTest { @Test fun givenIdentifierChanged_whenSendingIntent_thenStateIsUpdated() { - val viewModel = NewLoginIdentifierIosViewModelFactory(WireIosSharedConfig(serverLinks)).create() + val viewModel = newViewModel() viewModel.sendIntent(NewLoginIdentifierIntent.UserIdentifierChanged("user@example.com")) @@ -39,7 +40,7 @@ class NewLoginIdentifierIosViewModelFactoryTest { @Test fun givenInvalidIdentifier_whenSubmitting_thenTextFieldErrorIsShown() { - val viewModel = NewLoginIdentifierIosViewModelFactory(WireIosSharedConfig(serverLinks)).create() + val viewModel = newViewModel() viewModel.sendIntent(NewLoginIdentifierIntent.UserIdentifierChanged("invalid")) viewModel.sendIntent(NewLoginIdentifierIntent.Submit) @@ -52,23 +53,21 @@ class NewLoginIdentifierIosViewModelFactoryTest { @Test fun givenEmailIdentifier_whenSubmitting_thenOpenEmailPasswordEffectIsEmitted() = runTest { - val viewModel = NewLoginIdentifierIosViewModelFactory(WireIosSharedConfig(serverLinks)).create() + val viewModel = newViewModel() viewModel.sendIntent(NewLoginIdentifierIntent.UserIdentifierChanged("user@example.com")) val effect = async(start = CoroutineStart.UNDISPATCHED) { - var observedEffect: NewLoginIdentifierEffect? = null - val closeable = viewModel.observeEffect { observedEffect = it } - viewModel.sendIntent(NewLoginIdentifierIntent.Submit) - closeable.close() - observedEffect + viewModel.effects.first() } + viewModel.sendIntent(NewLoginIdentifierIntent.Submit) + val openEmailPassword = assertIs(effect.await()) assertEquals("user@example.com", openEmailPassword.userIdentifier) } @Test fun givenSsoFailure_whenReceived_thenDialogErrorIsShown() { - val viewModel = NewLoginIdentifierIosViewModelFactory(WireIosSharedConfig(serverLinks)).create() + val viewModel = newViewModel() viewModel.sendIntent(NewLoginIdentifierIntent.SSOResultReceived(NewLoginSsoResult.Failure(NewLoginSsoFailureCode.InvalidCode))) @@ -81,6 +80,12 @@ class NewLoginIdentifierIosViewModelFactoryTest { } private companion object { + fun newViewModel(): NewLoginIdentifierIosViewModel = + NewLoginIdentifierIosViewModelFactory( + config = WireIosSharedConfig(serverLinks), + backend = LocalNewLoginIdentifierBackend(), + ).create() + val serverLinks = LoginServerLinks( api = "https://api.example.com", accounts = "https://accounts.example.com", diff --git a/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailBackend.kt b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailBackend.kt new file mode 100644 index 00000000000..606107a92d0 --- /dev/null +++ b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailBackend.kt @@ -0,0 +1,213 @@ +/* + * 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.IosKaliumRuntimeConfig +import com.wire.ios.shared.WireIosSharedConfig +import com.wire.ios.shared.auth.login.model.toKalium +import com.wire.kalium.logic.CoreLogicCommon +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.GetOrRegisterClientUseCase +import com.wire.kalium.logic.feature.client.RegisterClientParam +import com.wire.kalium.logic.feature.client.RegisterClientResult +import dev.zacsweers.metro.Inject + +@Inject +class KaliumLoginEmailBackend( + private val config: WireIosSharedConfig, + private val coreLogic: CoreLogicCommon, +) : LoginEmailBackend { + private val runtimeConfig: IosKaliumRuntimeConfig? + get() = config.runtimeConfig + + private val localBackend = LocalLoginEmailBackend() + + override suspend fun login( + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + usernameAllowed: Boolean, + ): LoginEmailBackendResult { + val runtime = runtimeConfig ?: return localBackend.login( + userIdentifier = userIdentifier, + password = password, + secondFactorVerificationCode = secondFactorVerificationCode, + usernameAllowed = usernameAllowed, + ) + if (!usernameAllowed && !coreLogic.getGlobalScope().validateEmailUseCase(userIdentifier)) { + return LoginEmailBackendResult.Failure(LoginEmailError.InvalidUserIdentifier) + } + + val authScope = when (val result = resolveAuthScope(runtime)) { + is AuthScopeResult.Failure -> return LoginEmailBackendResult.Failure(result.error) + is AuthScopeResult.Success -> result.authScope + } + val loginResult = authScope.login( + userIdentifier = userIdentifier, + password = password, + shouldPersistClient = true, + secondFactorVerificationCode = secondFactorVerificationCode, + ) + if (loginResult !is AuthenticationResult.Success) { + return handleAuthenticationFailure(loginResult as AuthenticationResult.Failure, authScope, userIdentifier) + } + + val storedUserId = addAuthenticatedUser(loginResult) + ?: return LoginEmailBackendResult.Failure(LoginEmailError.UserAlreadyExists) + + if (coreLogic.getGlobalScope().validateEmailUseCase(userIdentifier)) { + val persistEmailResult = coreLogic.getSessionScope(storedUserId).users.persistSelfUserEmail(userIdentifier) + if (persistEmailResult is PersistSelfUserEmailResult.Failure) { + return LoginEmailBackendResult.Failure(LoginEmailError.Generic(persistEmailResult.coreFailure.toString())) + } + } + + return when ( + val clientResult = coreLogic.getSessionScope(storedUserId).client.getOrRegister( + RegisterClientParam(password = password, capabilities = null) + ) + ) { + is RegisterClientResult.Success -> + LoginEmailBackendResult.Success(initialSyncCompleted = false, isE2EIRequired = false) + + is RegisterClientResult.E2EICertificateRequired -> + LoginEmailBackendResult.Success(initialSyncCompleted = false, isE2EIRequired = true) + + is RegisterClientResult.Failure.TooManyClients -> + LoginEmailBackendResult.RemoveDeviceNeeded + + is RegisterClientResult.Failure -> + LoginEmailBackendResult.Failure(clientResult.toLoginEmailError()) + } + } + + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult { + val runtime = runtimeConfig ?: return localBackend.requestSecondFactorCode(userIdentifier) + val authScope = when (val result = resolveAuthScope(runtime)) { + is AuthScopeResult.Failure -> return LoginEmailBackendResult.Failure(result.error) + is AuthScopeResult.Success -> result.authScope + } + return requestSecondFactorCode(authScope, userIdentifier) + } + + 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, + ): LoginEmailBackendResult = + when (failure) { + AuthenticationResult.Failure.InvalidCredentials.Missing2FA -> + requestSecondFactorCode(authScope, userIdentifier) + + AuthenticationResult.Failure.InvalidCredentials.Invalid2FA -> + LoginEmailBackendResult.SecondFactorRequired( + email = userIdentifier, + isCurrentCodeInvalid = true, + ) + + else -> LoginEmailBackendResult.Failure(failure.toLoginEmailError()) + } + + private suspend fun requestSecondFactorCode( + authScope: AuthenticationScope, + userIdentifier: String, + ): LoginEmailBackendResult { + if (!userIdentifier.contains("@")) { + return LoginEmailBackendResult.Failure(LoginEmailError.RequestSecondFactorWithHandle) + } + return when ( + val result = authScope.requestSecondFactorVerificationCode( + email = userIdentifier, + verifiableAction = VerifiableAction.LOGIN_OR_CLIENT_REGISTRATION, + ) + ) { + RequestSecondFactorVerificationCodeUseCase.Result.Success, + RequestSecondFactorVerificationCodeUseCase.Result.Failure.TooManyRequests -> + LoginEmailBackendResult.SecondFactorRequired(email = userIdentifier) + + is RequestSecondFactorVerificationCodeUseCase.Result.Failure.Generic -> + LoginEmailBackendResult.Failure(LoginEmailError.Generic(result.cause.toString())) + + else -> LoginEmailBackendResult.Failure(LoginEmailError.Generic()) + } + } +} + +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 + } 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..47dd6b22183 --- /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.ios.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..2f203d1d3ff --- /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.ios.shared.auth.newlogin + +import com.wire.ios.shared.IosKaliumRuntimeConfig +import com.wire.ios.shared.WireIosSharedConfig +import com.wire.ios.shared.auth.login.model.LoginApiProxy +import com.wire.ios.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 index e47af191534..a23d92b3098 100644 --- 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 @@ -17,10 +17,23 @@ */ 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.ios.shared.auth.email.KaliumLoginEmailBackend +import com.wire.ios.shared.auth.email.LoginEmailBackend +import com.wire.ios.shared.auth.email.LoginEmailEffect +import com.wire.ios.shared.auth.email.LoginEmailIntent +import com.wire.ios.shared.auth.email.LoginEmailIosViewModel +import com.wire.ios.shared.auth.email.LoginEmailIosViewModelFactory +import com.wire.ios.shared.auth.email.LoginEmailState +import com.wire.ios.shared.auth.email.createGenericLoginEmailIosViewModel +import com.wire.ios.shared.auth.email.createLoginEmailIosViewModel import com.wire.ios.shared.auth.login.model.LoginServerLinks +import com.wire.ios.shared.auth.newlogin.KaliumNewLoginIdentifierBackend +import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierBackend import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierEffect import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierIntent import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierIosViewModel @@ -35,8 +48,12 @@ import com.wire.ios.shared.auth.welcome.WelcomeIosViewModelFactory import com.wire.ios.shared.auth.welcome.WelcomeState 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 dev.zacsweers.metro.DependencyGraph import dev.zacsweers.metro.Provides +import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.createGraphFactory @DependencyGraph(WireIosSharedScope::class) @@ -46,6 +63,22 @@ interface WireIosSharedGraph { 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 provideWelcomeIosViewModel( welcomeIosViewModelFactory: WelcomeIosViewModelFactory, @@ -70,10 +103,44 @@ interface WireIosSharedGraph { ): 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 provideLoginEmailBackend( + backend: KaliumLoginEmailBackend, + ): LoginEmailBackend = backend + val welcomeViewModel: WelcomeIosViewModel val welcomeIosViewModel: IosViewModel val newLoginIdentifierViewModel: NewLoginIdentifierIosViewModel val newLoginIdentifierIosViewModel: IosViewModel + val loginEmailViewModel: LoginEmailIosViewModel + val loginEmailIosViewModel: 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 = @@ -83,3 +150,14 @@ fun createWireIosShared(defaultServerLinks: LoginServerLinks): WireIosSharedGrap createWireIosSharedGraph( config = com.wire.ios.shared.createWireIosSharedConfig(defaultServerLinks) ) + +fun createWireIosSharedProbe( + defaultServerLinks: LoginServerLinks, + runtimeConfig: IosKaliumRuntimeConfig? = null, +): WireIosSharedGraph = + createWireIosSharedGraph( + config = com.wire.ios.shared.createWireIosSharedConfig( + defaultServerLinks = defaultServerLinks, + runtimeConfig = runtimeConfig, + ) + ) From 8bd9aee20b7efa03f706cfc8e3fdf8bd29bf8930 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Mon, 18 May 2026 12:47:38 +0200 Subject: [PATCH 42/46] feat: add shared auth flow for iOS handoff --- app/build.gradle.kts | 1 + .../login/email/LoginEmailViewModel.kt | 26 ++ .../email/SharedAuthLoginEmailAdapter.kt | 57 ++++ .../login/NewLoginViewModel.kt | 39 +++ .../login/SharedAuthNewLoginAdapter.kt | 59 ++++ shared/auth/build.gradle.kts | 25 ++ .../shared/auth/AuthLoginSuccessPayload.kt | 43 +++ .../com/wire/shared/auth/CoroutineScopeExt.kt | 25 ++ .../kotlin/com/wire/shared/auth/NoEffect.kt | 20 ++ .../com/wire/shared/auth/SharedAuthConfig.kt | 29 ++ .../com/wire/shared/auth/SharedViewModel.kt | 99 +++++++ .../shared/auth/email/LoginEmailContract.kt | 41 ++- .../shared/auth/email/LoginEmailGateway.kt | 85 ++++++ .../auth/email/LoginEmailViewModelFactory.kt | 223 ++++++++++++++ .../shared/auth/flow/AuthLoginFlowBackend.kt | 98 ++++++ .../shared/auth/flow/AuthLoginFlowContract.kt | 89 ++++++ .../flow/AuthLoginFlowViewModelFactory.kt | 269 +++++++++++++++++ .../shared/auth/login/model/LoginNavArgs.kt | 2 +- .../login/model/LoginNavigationCommand.kt | 2 +- .../auth/login/model/LoginPasswordPath.kt | 2 +- .../auth/login/model/LoginScreenState.kt | 2 +- .../auth/login/model/LoginServerLinks.kt | 2 +- .../newlogin/NewLoginIdentifierBackend.kt | 12 +- .../newlogin/NewLoginIdentifierContract.kt | 32 +- .../NewLoginIdentifierStateReducer.kt | 2 +- .../NewLoginIdentifierViewModelFactory.kt | 187 ++++++++++++ .../wire/shared/auth/sso/LoginSsoBackend.kt | 43 +++ .../wire}/shared/auth/sso/LoginSsoContract.kt | 26 +- .../auth/sso/LoginSsoViewModelFactory.kt | 173 +++++++++++ .../shared/auth/welcome/WelcomeEffect.kt | 4 +- .../shared/auth/welcome/WelcomeIntent.kt | 2 +- .../wire}/shared/auth/welcome/WelcomeState.kt | 4 +- .../auth/welcome/WelcomeViewModelFactory.kt | 89 ++++++ .../auth/email/LoginEmailContractTest.kt | 35 ++- .../email/LoginEmailViewModelFactoryTest.kt | 279 ++++++++++++++++++ .../flow/AuthLoginFlowViewModelFactoryTest.kt | 164 ++++++++++ .../auth/login/model/LoginNavArgsTest.kt | 2 +- .../NewLoginIdentifierStateReducerTest.kt | 38 ++- .../NewLoginIdentifierViewModelFactoryTest.kt | 169 +++++++++++ .../shared/auth/sso/LoginSsoContractTest.kt | 6 +- .../auth/sso/LoginSsoViewModelFactoryTest.kt | 131 ++++++++ .../auth/welcome/WelcomeContractTest.kt | 6 +- .../welcome/WelcomeViewModelFactoryTest.kt} | 18 +- shared/export-ios/build.gradle.kts | 2 + .../wire/ios/shared/WireIosSharedConfig.kt | 2 +- .../shared/auth/email/LoginEmailBackend.kt | 60 ---- .../auth/email/LoginEmailIosViewModel.kt | 211 +------------ .../auth/flow/AuthLoginFlowIosViewModel.kt | 80 +++++ .../NewLoginIdentifierIosViewModel.kt | 201 +------------ .../shared/auth/sso/LoginSsoIosViewModel.kt | 80 +++++ .../auth/welcome/WelcomeIosViewModel.kt | 64 +--- .../LoginEmailIosViewModelFactoryTest.kt | 119 -------- ...wLoginIdentifierIosViewModelFactoryTest.kt | 100 ------- ...lBackend.kt => KaliumLoginEmailGateway.kt} | 117 ++++++-- .../auth/flow/KaliumAuthLoginFlowBackend.kt | 114 +++++++ .../model/LoginServerLinksKaliumMapper.kt | 2 +- .../KaliumNewLoginIdentifierBackend.kt | 6 +- .../ios/shared/export/WireIosSharedGraph.kt | 103 +++++-- 58 files changed, 3097 insertions(+), 824 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/authentication/login/email/SharedAuthLoginEmailAdapter.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/newauthentication/login/SharedAuthNewLoginAdapter.kt create mode 100644 shared/auth/build.gradle.kts create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/AuthLoginSuccessPayload.kt create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/CoroutineScopeExt.kt create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/NoEffect.kt create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/SharedAuthConfig.kt create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/SharedViewModel.kt rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/email/LoginEmailContract.kt (77%) create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailGateway.kt create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailViewModelFactory.kt create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowBackend.kt create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowContract.kt create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/flow/AuthLoginFlowViewModelFactory.kt rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/login/model/LoginNavArgs.kt (97%) rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/login/model/LoginNavigationCommand.kt (97%) rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/login/model/LoginPasswordPath.kt (96%) rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/login/model/LoginScreenState.kt (97%) rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/login/model/LoginServerLinks.kt (96%) rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/newlogin/NewLoginIdentifierBackend.kt (81%) rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/newlogin/NewLoginIdentifierContract.kt (79%) rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt (97%) create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierViewModelFactory.kt create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoBackend.kt rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/sso/LoginSsoContract.kt (86%) create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoViewModelFactory.kt rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/welcome/WelcomeEffect.kt (92%) rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/welcome/WelcomeIntent.kt (95%) rename shared/{export-ios/src/commonMain/kotlin/com/wire/ios => auth/src/commonMain/kotlin/com/wire}/shared/auth/welcome/WelcomeState.kt (90%) create mode 100644 shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactory.kt rename shared/{export-ios/src/commonTest/kotlin/com/wire/ios => auth/src/commonTest/kotlin/com/wire}/shared/auth/email/LoginEmailContractTest.kt (59%) create mode 100644 shared/auth/src/commonTest/kotlin/com/wire/shared/auth/email/LoginEmailViewModelFactoryTest.kt create mode 100644 shared/auth/src/commonTest/kotlin/com/wire/shared/auth/flow/AuthLoginFlowViewModelFactoryTest.kt rename shared/{export-ios/src/commonTest/kotlin/com/wire/ios => auth/src/commonTest/kotlin/com/wire}/shared/auth/login/model/LoginNavArgsTest.kt (97%) rename shared/{export-ios/src/commonTest/kotlin/com/wire/ios => auth/src/commonTest/kotlin/com/wire}/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt (55%) create mode 100644 shared/auth/src/commonTest/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierViewModelFactoryTest.kt rename shared/{export-ios/src/commonTest/kotlin/com/wire/ios => auth/src/commonTest/kotlin/com/wire}/shared/auth/sso/LoginSsoContractTest.kt (93%) create mode 100644 shared/auth/src/commonTest/kotlin/com/wire/shared/auth/sso/LoginSsoViewModelFactoryTest.kt rename shared/{export-ios/src/commonTest/kotlin/com/wire/ios => auth/src/commonTest/kotlin/com/wire}/shared/auth/welcome/WelcomeContractTest.kt (94%) rename shared/{export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt => auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactoryTest.kt} (83%) delete mode 100644 shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailBackend.kt create mode 100644 shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/flow/AuthLoginFlowIosViewModel.kt create mode 100644 shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoIosViewModel.kt delete mode 100644 shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModelFactoryTest.kt delete mode 100644 shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt rename shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/{KaliumLoginEmailBackend.kt => KaliumLoginEmailGateway.kt} (68%) create mode 100644 shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/flow/KaliumAuthLoginFlowBackend.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 572c6d62e30..84f7dcfd07f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -201,6 +201,7 @@ dependencies { implementationWithCoverage(projects.features.sketch) implementationWithCoverage(projects.features.meetings) implementationWithCoverage(projects.features.sync) + implementationWithCoverage(projects.shared.auth) // Anonymous Analytics val flavors = getFlavorsSettings() 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 0273ee26fa7..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 @@ -80,6 +80,7 @@ class LoginEmailViewModel constructor( private val dispatchers: DispatcherProvider, defaultServerConfig: ServerConfig.Links, @DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean, + private val sharedAuthLoginEmailAdapter: SharedAuthLoginEmailAdapter = LegacySharedAuthLoginEmailAdapter, ) : LoginViewModel( loginNavArgs, clientScopeProviderFactory, @@ -160,6 +161,7 @@ class LoginEmailViewModel 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 @@ -172,6 +174,30 @@ class LoginEmailViewModel 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 { 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/newauthentication/login/NewLoginViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt index 88b88463f8a..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 @@ -78,6 +78,7 @@ class NewLoginViewModel( defaultServerConfig: ServerConfig.Links, defaultSSOCodeConfig: String, private val recoverableLogoutExceptionDetector: NewLoginRecoverableLogoutExceptionDetector, + private val sharedAuthNewLoginAdapter: SharedAuthNewLoginAdapter = LegacySharedAuthNewLoginAdapter, ) : ActionsViewModel() { private val preFilledUserIdentifier: PreFilledUserIdentifierType = loginNavArgs.userHandle ?: PreFilledUserIdentifierType.None @@ -146,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) @@ -163,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( 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/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/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailContract.kt similarity index 77% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailContract.kt index 7d9730282fe..dccf9858f25 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailContract.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailContract.kt @@ -15,7 +15,7 @@ * 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 +package com.wire.shared.auth.email data class LoginEmailState( val userIdentifier: String = "", @@ -26,7 +26,37 @@ data class LoginEmailState( 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 = "", @@ -69,11 +99,16 @@ sealed interface 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 @@ -81,7 +116,7 @@ sealed interface LoginEmailIntent { } /** - * One-shot login actions for Swift hosts. + * 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]. 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..d2c563a3280 --- /dev/null +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/email/LoginEmailViewModelFactory.kt @@ -0,0 +1,223 @@ +/* + * 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 -> + when (intent) { + is LoginEmailIntent.UserIdentifierChanged -> + state.update { + it.copy( + userIdentifier = intent.value, + loginEnabled = it.canSubmit(userIdentifier = intent.value), + flowState = LoginEmailFlowState.Default, + ) + } + + is LoginEmailIntent.PasswordChanged -> + state.update { + it.copy( + password = intent.value, + loginEnabled = it.canSubmit(password = intent.value), + flowState = LoginEmailFlowState.Default, + ) + } + + is LoginEmailIntent.ProxyIdentifierChanged -> + state.update { it.copy(proxyIdentifier = intent.value) } + + is LoginEmailIntent.ProxyPasswordChanged -> + state.update { it.copy(proxyPassword = intent.value) } + + is LoginEmailIntent.SecondFactorCodeChanged -> + state.update { + it.copy( + secondFactorVerificationCode = it.secondFactorVerificationCode.copy( + code = intent.value, + isCurrentCodeInvalid = false, + ) + ) + } + + 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 -> + state.update { + it.copy( + secondFactorVerificationCode = LoginEmailVerificationCodeState(), + flowState = LoginEmailFlowState.Default, + ) + } + + LoginEmailIntent.ResendSecondFactorCode -> + requestSecondFactorCode(state, scope) + } + }, + onClose = { scope.cancelScope() }, + ) + } + + 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 { + when ( + val result = gateway.login( + userIdentifier = currentState.userIdentifier, + password = currentState.password, + secondFactorVerificationCode = currentState.secondFactorVerificationCode.code.takeIf { it.isNotBlank() }, + usernameAllowed = usernameAllowed, + ) + ) { + is LoginEmailGatewayResult.Failure -> { + state.update { + val flowState = LoginEmailFlowState.Error(result.error) + it.copy( + flowState = flowState, + loginEnabled = it.canSubmit(flowState = flowState), + ) + } + } + + LoginEmailGatewayResult.RemoveDeviceNeeded -> { + effects.tryEmit(LoginEmailEffect.RemoveDeviceNeeded) + state.update { + val flowState = LoginEmailFlowState.Error(LoginEmailError.TooManyDevices) + it.copy( + flowState = flowState, + loginEnabled = it.canSubmit(flowState = flowState), + ) + } + } + + is 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, + ), + ) + } + } + + is 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/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavArgs.kt similarity index 97% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavArgs.kt index 3d36acb40f5..789ab183759 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgs.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavArgs.kt @@ -15,7 +15,7 @@ * 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.login.model +package com.wire.shared.auth.login.model data class LoginNavArgs( val userIdentifier: LoginUserIdentifier = LoginUserIdentifier.None, diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavigationCommand.kt similarity index 97% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavigationCommand.kt index a06cbdd8dd8..795c9b18f70 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginNavigationCommand.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginNavigationCommand.kt @@ -15,7 +15,7 @@ * 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.login.model +package com.wire.shared.auth.login.model fun interface LoginNavigator { fun navigate(command: LoginNavigationCommand) diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginPasswordPath.kt similarity index 96% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginPasswordPath.kt index cec8b633739..f497c022ea3 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginPasswordPath.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginPasswordPath.kt @@ -15,7 +15,7 @@ * 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.login.model +package com.wire.shared.auth.login.model data class LoginPasswordPath( val customServerLinks: LoginServerLinks? = null, diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginScreenState.kt similarity index 97% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginScreenState.kt index 0248502b2a0..64a7c2f717b 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginScreenState.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginScreenState.kt @@ -15,7 +15,7 @@ * 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.login.model +package com.wire.shared.auth.login.model data class LoginScreenState( val isThereActiveSession: Boolean = false, diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginServerLinks.kt similarity index 96% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginServerLinks.kt index 356065d6817..a814cbfd9f0 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/login/model/LoginServerLinks.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/login/model/LoginServerLinks.kt @@ -15,7 +15,7 @@ * 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.login.model +package com.wire.shared.auth.login.model data class LoginServerLinks( val api: String, diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierBackend.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierBackend.kt similarity index 81% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierBackend.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierBackend.kt index 21e7df45d08..3918e0074ee 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierBackend.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierBackend.kt @@ -15,18 +15,10 @@ * 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 +package com.wire.shared.auth.newlogin -import com.wire.ios.shared.auth.login.model.LoginServerLinks +import com.wire.shared.auth.login.model.LoginServerLinks -/** - * Pilot adapter for the export-ios login probe. - * - * This keeps the current POC easy to exercise from Swift while the real migration shape is - * validated. It is not the target pattern for migrating Android ViewModels; shared ViewModels - * should eventually depend on Kalium use cases/facades directly through Metro, with only - * platform-specific capabilities extracted behind small gateways. - */ interface NewLoginIdentifierBackend { suspend fun resolveEmail(userIdentifier: String): NewLoginIdentifierBackendResult diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierContract.kt similarity index 79% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierContract.kt index 1d2e0533cca..290991f9c40 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierContract.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierContract.kt @@ -15,15 +15,12 @@ * 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 +package com.wire.shared.auth.newlogin -import com.wire.ios.shared.auth.login.model.LoginServerLinks +import com.wire.shared.auth.login.model.LoginServerLinks /** - * Swift-facing state for the first new-login screen where the user enters an email or SSO code. - * - * This mirrors the Android new-login state without exposing Android, Compose navigation, - * SavedStateHandle, or Kalium-specific error types. + * Platform-neutral state for the first new-login screen where the user enters an email or SSO code. */ data class NewLoginIdentifierState( val userIdentifier: String = "", @@ -31,7 +28,28 @@ data class NewLoginIdentifierState( 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 diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt similarity index 97% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt index e5d53f67792..5bf0d2ecc83 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducer.kt @@ -15,7 +15,7 @@ * 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 +package com.wire.shared.auth.newlogin fun NewLoginIdentifierState.withUserIdentifier(userIdentifier: String): NewLoginIdentifierState { val newFlowState = when (flowState) { 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/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoContract.kt similarity index 86% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoContract.kt index 9cf5e7a05d5..73a3b2c32c9 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContract.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/sso/LoginSsoContract.kt @@ -15,21 +15,28 @@ * 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 +package com.wire.shared.auth.sso -import com.wire.ios.shared.auth.login.model.LoginServerLinks +import com.wire.shared.auth.login.model.LoginServerLinks /** - * Swift-facing state for the SSO login screen. - * - * This mirrors the Android SSO screen state without exposing Compose, Android, or Kalium UI types. + * 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 @@ -62,26 +69,31 @@ sealed interface 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] using an iOS gateway and route the callback back as an intent. + * The platform layer should open [url] and route the callback back as an intent. */ data class OpenUrl( val url: String, 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/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeEffect.kt similarity index 92% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeEffect.kt index b29ebb5504b..487380db78f 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeEffect.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeEffect.kt @@ -15,9 +15,9 @@ * 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 +package com.wire.shared.auth.welcome -import com.wire.ios.shared.auth.login.model.LoginServerLinks +import com.wire.shared.auth.login.model.LoginServerLinks sealed interface WelcomeEffect { data class NavigateToLogin(val links: LoginServerLinks) : WelcomeEffect diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeIntent.kt similarity index 95% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeIntent.kt index 498a9941c66..7aa5b871135 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIntent.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeIntent.kt @@ -15,7 +15,7 @@ * 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 +package com.wire.shared.auth.welcome sealed interface WelcomeIntent { data object LoginClicked : WelcomeIntent diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeState.kt similarity index 90% rename from shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt rename to shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeState.kt index c10119a01bd..02d7a83d5cc 100644 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/welcome/WelcomeState.kt +++ b/shared/auth/src/commonMain/kotlin/com/wire/shared/auth/welcome/WelcomeState.kt @@ -15,9 +15,9 @@ * 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 +package com.wire.shared.auth.welcome -import com.wire.ios.shared.auth.login.model.LoginServerLinks +import com.wire.shared.auth.login.model.LoginServerLinks data class WelcomeState( val links: LoginServerLinks, 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/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/email/LoginEmailContractTest.kt similarity index 59% rename from shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt rename to shared/auth/src/commonTest/kotlin/com/wire/shared/auth/email/LoginEmailContractTest.kt index 2a18423a32d..7ca02d3a7f9 100644 --- a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailContractTest.kt +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/email/LoginEmailContractTest.kt @@ -15,7 +15,7 @@ * 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 +package com.wire.shared.auth.email import kotlin.test.Test import kotlin.test.assertEquals @@ -31,14 +31,47 @@ class LoginEmailContractTest { 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 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/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/login/model/LoginNavArgsTest.kt similarity index 97% rename from shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt rename to shared/auth/src/commonTest/kotlin/com/wire/shared/auth/login/model/LoginNavArgsTest.kt index 0fcae244a45..aa4db7d2eaf 100644 --- a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/login/model/LoginNavArgsTest.kt +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/login/model/LoginNavArgsTest.kt @@ -15,7 +15,7 @@ * 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.login.model +package com.wire.shared.auth.login.model import kotlin.test.Test import kotlin.test.assertFalse diff --git a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt similarity index 55% rename from shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt rename to shared/auth/src/commonTest/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt index 6b066cd14c5..f347e30b952 100644 --- a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/newlogin/NewLoginIdentifierStateReducerTest.kt @@ -15,11 +15,12 @@ * 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 +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 { @@ -31,6 +32,13 @@ class NewLoginIdentifierStateReducerTest { 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 @@ -40,6 +48,8 @@ class NewLoginIdentifierStateReducerTest { ).withUserIdentifier("user@example.com") assertEquals(NewLoginIdentifierFlowState.Default, state.flowState) + assertFalse(state.hasTextFieldError) + assertNull(state.textFieldError) } @Test @@ -48,5 +58,31 @@ class NewLoginIdentifierStateReducerTest { .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/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/sso/LoginSsoContractTest.kt similarity index 93% rename from shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt rename to shared/auth/src/commonTest/kotlin/com/wire/shared/auth/sso/LoginSsoContractTest.kt index fdcf84f0959..43eaa509683 100644 --- a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/sso/LoginSsoContractTest.kt +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/sso/LoginSsoContractTest.kt @@ -15,9 +15,9 @@ * 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 +package com.wire.shared.auth.sso -import com.wire.ios.shared.auth.login.model.LoginServerLinks +import com.wire.shared.auth.login.model.LoginServerLinks import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -37,7 +37,7 @@ class LoginSsoContractTest { } @Test - fun givenOpenUrlEffect_whenCreated_thenItCarriesOnlySwiftFacingData() { + fun givenOpenUrlEffect_whenCreated_thenItCarriesOnlyPlatformFacingData() { val effect = LoginSsoEffect.OpenUrl( url = "wire://sso/start", serverLinks = serverLinks, 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/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeContractTest.kt similarity index 94% rename from shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt rename to shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeContractTest.kt index eecae40c7a2..9f02208e6ab 100644 --- a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeContractTest.kt +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeContractTest.kt @@ -15,10 +15,10 @@ * 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 +package com.wire.shared.auth.welcome -import com.wire.ios.shared.auth.login.model.LoginApiProxy -import com.wire.ios.shared.auth.login.model.LoginServerLinks +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 diff --git a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactoryTest.kt similarity index 83% rename from shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt rename to shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactoryTest.kt index a77f1257785..2a21930bac5 100644 --- a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/welcome/WelcomeIosViewModelFactoryTest.kt +++ b/shared/auth/src/commonTest/kotlin/com/wire/shared/auth/welcome/WelcomeViewModelFactoryTest.kt @@ -15,11 +15,11 @@ * 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 +package com.wire.shared.auth.welcome -import com.wire.ios.shared.WireIosSharedConfig -import com.wire.ios.shared.auth.login.model.LoginApiProxy -import com.wire.ios.shared.auth.login.model.LoginServerLinks +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 @@ -28,11 +28,11 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs -class WelcomeIosViewModelFactoryTest { +class WelcomeViewModelFactoryTest { @Test fun givenConfig_whenCreatingViewModel_thenStateUsesConfig() { - val viewModel = WelcomeIosViewModelFactory( - config = WireIosSharedConfig( + val viewModel = WelcomeViewModelFactory( + config = SharedAuthConfig( defaultServerLinks = serverLinks, maxAccountsReached = true, ) @@ -44,7 +44,7 @@ class WelcomeIosViewModelFactoryTest { @Test fun givenLoginClicked_whenSendingIntent_thenNavigateToLoginEffectIsEmitted() = runTest { - val viewModel = WelcomeIosViewModelFactory(WireIosSharedConfig(serverLinks)).create() + val viewModel = WelcomeViewModelFactory(SharedAuthConfig(serverLinks)).create() val effect = async(start = CoroutineStart.UNDISPATCHED) { viewModel.effects.first() } viewModel.sendIntent(WelcomeIntent.LoginClicked) @@ -61,7 +61,7 @@ class WelcomeIosViewModelFactoryTest { port = 8080, ) ) - val viewModel = WelcomeIosViewModelFactory(WireIosSharedConfig(links)).create() + val viewModel = WelcomeViewModelFactory(SharedAuthConfig(links)).create() val effect = async(start = CoroutineStart.UNDISPATCHED) { viewModel.effects.first() } viewModel.sendIntent(WelcomeIntent.CreatePersonalAccountClicked) diff --git a/shared/export-ios/build.gradle.kts b/shared/export-ios/build.gradle.kts index 2b19acd91f9..cfc68da0a98 100644 --- a/shared/export-ios/build.gradle.kts +++ b/shared/export-ios/build.gradle.kts @@ -11,6 +11,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + api(projects.shared.auth) api(libs.coroutines.core) implementation("com.wire.kalium:kalium-logic") } @@ -28,6 +29,7 @@ kotlin { binaries.framework { baseName = "WireIosShared" isStatic = false + export(projects.shared.auth) } } } 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 index c0e66c8bad7..d93720b2e0d 100644 --- 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 @@ -17,7 +17,7 @@ */ package com.wire.ios.shared -import com.wire.ios.shared.auth.login.model.LoginServerLinks +import com.wire.shared.auth.login.model.LoginServerLinks data class WireIosSharedConfig( val defaultServerLinks: LoginServerLinks, diff --git a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailBackend.kt b/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailBackend.kt deleted file mode 100644 index a9227fa047d..00000000000 --- a/shared/export-ios/src/commonMain/kotlin/com/wire/ios/shared/auth/email/LoginEmailBackend.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Wire - * Copyright (C) 2026 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.ios.shared.auth.email - -interface LoginEmailBackend { - suspend fun login( - userIdentifier: String, - password: String, - secondFactorVerificationCode: String?, - usernameAllowed: Boolean, - ): LoginEmailBackendResult - - suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult -} - -sealed interface LoginEmailBackendResult { - data class Success( - val initialSyncCompleted: Boolean, - val isE2EIRequired: Boolean, - ) : LoginEmailBackendResult - - data class SecondFactorRequired( - val email: String, - val isCurrentCodeInvalid: Boolean = false, - ) : LoginEmailBackendResult - - data object RemoveDeviceNeeded : LoginEmailBackendResult - data class Failure(val error: LoginEmailError) : LoginEmailBackendResult -} - -class LocalLoginEmailBackend : LoginEmailBackend { - override suspend fun login( - userIdentifier: String, - password: String, - secondFactorVerificationCode: String?, - usernameAllowed: Boolean, - ): LoginEmailBackendResult = - LoginEmailBackendResult.Success( - initialSyncCompleted = false, - isE2EIRequired = false, - ) - - override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult = - LoginEmailBackendResult.SecondFactorRequired(email = userIdentifier) -} 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 index e5a65f279dd..368879bd6ee 100644 --- 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 @@ -20,17 +20,12 @@ 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.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -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 class LoginEmailIosViewModel( private val delegate: IosViewModel, @@ -58,207 +53,33 @@ class LoginEmailIosViewModel( @Inject class LoginEmailIosViewModelFactory( - private val backend: LoginEmailBackend, + private val sharedFactory: LoginEmailViewModelFactory, ) { fun create(userIdentifier: String = ""): LoginEmailIosViewModel = LoginEmailIosViewModel(createGeneric(userIdentifier)) fun createGeneric(userIdentifier: String = ""): IosViewModel { - val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) - val state = MutableStateFlow( - LoginEmailState( - userIdentifier = userIdentifier, - userIdentifierEnabled = userIdentifier.isBlank(), - loginEnabled = false, - ) + val sharedViewModel = sharedFactory.create( + userIdentifier = userIdentifier, + coroutineContext = Dispatchers.Main.immediate, ) - val effects = MutableSharedFlow(extraBufferCapacity = 1) - return IosViewModel( - state = state.asStateFlow(), - effects = effects.asSharedFlow(), - onIntent = { intent -> - when (intent) { - is LoginEmailIntent.UserIdentifierChanged -> - state.update { - it.copy( - userIdentifier = intent.value, - loginEnabled = it.canSubmit(userIdentifier = intent.value), - flowState = LoginEmailFlowState.Default, - ) - } - - is LoginEmailIntent.PasswordChanged -> - state.update { - it.copy( - password = intent.value, - loginEnabled = it.canSubmit(password = intent.value), - flowState = LoginEmailFlowState.Default, - ) - } - - is LoginEmailIntent.ProxyIdentifierChanged -> - state.update { it.copy(proxyIdentifier = intent.value) } - - is LoginEmailIntent.ProxyPasswordChanged -> - state.update { it.copy(proxyPassword = intent.value) } - - is LoginEmailIntent.SecondFactorCodeChanged -> - state.update { - it.copy( - secondFactorVerificationCode = it.secondFactorVerificationCode.copy( - code = intent.value, - isCurrentCodeInvalid = false, - ) - ) - } - - 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 -> - state.update { - it.copy( - secondFactorVerificationCode = LoginEmailVerificationCodeState(), - flowState = LoginEmailFlowState.Default, - ) - } - - LoginEmailIntent.ResendSecondFactorCode -> - requestSecondFactorCode(state, scope) - } - }, - onClose = { scope.close() }, + state = sharedViewModel.state, + effects = sharedViewModel.effects, + onIntent = sharedViewModel::sendIntent, + onClose = sharedViewModel::close, ) } - - 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 { - when ( - val result = backend.login( - userIdentifier = currentState.userIdentifier, - password = currentState.password, - secondFactorVerificationCode = currentState.secondFactorVerificationCode.code.takeIf { it.isNotBlank() }, - usernameAllowed = usernameAllowed, - ) - ) { - is LoginEmailBackendResult.Failure -> { - state.update { - val flowState = LoginEmailFlowState.Error(result.error) - it.copy( - flowState = flowState, - loginEnabled = it.canSubmit(flowState = flowState), - ) - } - } - - LoginEmailBackendResult.RemoveDeviceNeeded -> { - effects.tryEmit(LoginEmailEffect.RemoveDeviceNeeded) - state.update { - val flowState = LoginEmailFlowState.Error(LoginEmailError.TooManyDevices) - it.copy( - flowState = flowState, - loginEnabled = it.canSubmit(flowState = flowState), - ) - } - } - - is LoginEmailBackendResult.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, - ), - ) - } - } - - is LoginEmailBackendResult.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 = backend.requestSecondFactorCode(state.value.userIdentifier)) { - is LoginEmailBackendResult.Failure -> - state.update { it.copy(flowState = LoginEmailFlowState.Error(result.error)) } - - is LoginEmailBackendResult.SecondFactorRequired -> - state.update { - it.copy( - flowState = LoginEmailFlowState.Default, - secondFactorVerificationCode = it.secondFactorVerificationCode.copy( - isCodeInputNecessary = true, - emailUsed = result.email, - isCurrentCodeInvalid = result.isCurrentCodeInvalid, - ), - ) - } - - LoginEmailBackendResult.RemoveDeviceNeeded, - is LoginEmailBackendResult.Success -> - Unit - } - } - } } fun createLoginEmailIosViewModel( - factory: LoginEmailIosViewModelFactory, + loginEmailIosViewModelFactory: LoginEmailIosViewModelFactory, userIdentifier: String = "", ): LoginEmailIosViewModel = - factory.create(userIdentifier) + loginEmailIosViewModelFactory.create(userIdentifier) fun createGenericLoginEmailIosViewModel( - factory: LoginEmailIosViewModelFactory, + loginEmailIosViewModelFactory: LoginEmailIosViewModelFactory, userIdentifier: String = "", ): IosViewModel = - factory.createGeneric(userIdentifier) - -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 - -private fun CoroutineScope.close() { - coroutineContext[Job]?.cancel() -} + 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 index 9aa53bf3ef3..559d92655cb 100644 --- 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 @@ -20,18 +20,12 @@ 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.ios.shared.WireIosSharedConfig +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.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -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 class NewLoginIdentifierIosViewModel( private val delegate: IosViewModel, @@ -59,195 +53,28 @@ class NewLoginIdentifierIosViewModel( @Inject class NewLoginIdentifierIosViewModelFactory( - private val config: WireIosSharedConfig, - private val backend: NewLoginIdentifierBackend, + private val sharedFactory: NewLoginIdentifierViewModelFactory, ) { fun create(): NewLoginIdentifierIosViewModel = NewLoginIdentifierIosViewModel(createGeneric()) fun createGeneric(): IosViewModel { - val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) - val state = MutableStateFlow( - NewLoginIdentifierState( - isThereActiveSession = config.isThereActiveSession, - ) - ) - val effects = MutableSharedFlow(extraBufferCapacity = 1) - + val sharedViewModel = sharedFactory.create(coroutineContext = Dispatchers.Main.immediate) return IosViewModel( - state = state.asStateFlow(), - effects = effects.asSharedFlow(), - onIntent = { intent -> - when (intent) { - is NewLoginIdentifierIntent.UserIdentifierChanged -> { - state.update { it.withUserIdentifier(intent.userIdentifier) } - } - - NewLoginIdentifierIntent.Submit -> { - submit( - state = state, - effects = effects, - scope = 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( - result = intent.result, - state = state, - effects = effects, - ) - } - } - }, - onClose = { scope.close() }, + state = sharedViewModel.state, + effects = sharedViewModel.effects, + onIntent = sharedViewModel::sendIntent, + onClose = sharedViewModel::close, ) } - - private fun submit( - state: MutableStateFlow, - effects: MutableSharedFlow, - scope: CoroutineScope, - ) { - val userIdentifier = state.value.userIdentifier.trim() - when { - userIdentifier.isValidSsoCode() -> { - resolveWithBackend( - state = state, - effects = effects, - scope = scope, - resolve = { backend.initiateSso(userIdentifier) }, - ) - } - - userIdentifier.isValidEmail() -> { - resolveWithBackend( - state = state, - effects = effects, - scope = scope, - resolve = { 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 CoroutineScope.close() { - coroutineContext[Job]?.cancel() } fun createNewLoginIdentifierIosViewModel( - factory: NewLoginIdentifierIosViewModelFactory, + newLoginIdentifierIosViewModelFactory: NewLoginIdentifierIosViewModelFactory, ): NewLoginIdentifierIosViewModel = - factory.create() + newLoginIdentifierIosViewModelFactory.create() fun createGenericNewLoginIdentifierIosViewModel( - factory: NewLoginIdentifierIosViewModelFactory, + newLoginIdentifierIosViewModelFactory: NewLoginIdentifierIosViewModelFactory, ): IosViewModel = - factory.createGeneric() - -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}") + 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 index 74a87cf99fe..385ac6b4226 100644 --- 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 @@ -17,16 +17,14 @@ */ package com.wire.ios.shared.auth.welcome -import com.wire.ios.shared.IosViewModel import com.wire.ios.shared.IosCloseable import com.wire.ios.shared.IosObservableViewModel -import com.wire.ios.shared.WireIosSharedConfig -import com.wire.ios.shared.auth.login.model.LoginServerLinks +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 -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow class WelcomeIosViewModel( private val delegate: IosViewModel, @@ -54,60 +52,20 @@ class WelcomeIosViewModel( @Inject class WelcomeIosViewModelFactory( - private val config: WireIosSharedConfig, + private val sharedFactory: WelcomeViewModelFactory, ) { fun create(): WelcomeIosViewModel = WelcomeIosViewModel(createGeneric()) fun createGeneric(): IosViewModel { - 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) - + val sharedViewModel = sharedFactory.create() return IosViewModel( - 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 - } - } + state = sharedViewModel.state, + effects = sharedViewModel.effects, + onIntent = sharedViewModel::sendIntent, + onClose = sharedViewModel::close, ) } - - private fun createAccountEffect( - state: WelcomeState, - proxyLimitedTarget: WelcomeProxyLimitedTarget, - navigate: (links: LoginServerLinks) -> WelcomeEffect, - ): WelcomeEffect = - if (state.links.isProxyEnabled) { - WelcomeEffect.ShowProxyLimitation(proxyLimitedTarget) - } else { - navigate(state.links) - } } fun createWelcomeIosViewModel( diff --git a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModelFactoryTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModelFactoryTest.kt deleted file mode 100644 index 0a635c6ea1d..00000000000 --- a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/email/LoginEmailIosViewModelFactoryTest.kt +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Wire - * Copyright (C) 2026 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.ios.shared.auth.email - -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 - -class LoginEmailIosViewModelFactoryTest { - @Test - fun givenCredentials_whenSubmitting_thenLoginSucceededEffectIsEmitted() = runTest { - val viewModel = LoginEmailIosViewModelFactory(SuccessBackend).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()) - - assertIs(effect.await()) - assertEquals(LoginEmailFlowState.Success(initialSyncCompleted = false, isE2EIRequired = false), viewModel.currentState.flowState) - } - - @Test - fun givenSecondFactorRequired_whenSubmitting_thenVerificationCodeStateIsShown() = runTest { - val viewModel = LoginEmailIosViewModelFactory(SecondFactorBackend).create() - viewModel.sendIntent(LoginEmailIntent.UserIdentifierChanged("user@example.com")) - viewModel.sendIntent(LoginEmailIntent.PasswordChanged("password")) - - viewModel.sendIntent(LoginEmailIntent.SubmitLogin()) - - assertEquals(true, viewModel.currentState.secondFactorVerificationCode.isCodeInputNecessary) - assertEquals("user@example.com", viewModel.currentState.secondFactorVerificationCode.emailUsed) - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun givenInvalidCredentials_whenSubmitting_thenErrorStateIsShownWithoutEffect() = runTest { - val viewModel = LoginEmailIosViewModelFactory(InvalidCredentialsBackend).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 SuccessBackend : LoginEmailBackend { - override suspend fun login( - userIdentifier: String, - password: String, - secondFactorVerificationCode: String?, - usernameAllowed: Boolean, - ): LoginEmailBackendResult = - LoginEmailBackendResult.Success(initialSyncCompleted = false, isE2EIRequired = false) - - override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult = - LoginEmailBackendResult.SecondFactorRequired(userIdentifier) - } - - private object SecondFactorBackend : LoginEmailBackend { - override suspend fun login( - userIdentifier: String, - password: String, - secondFactorVerificationCode: String?, - usernameAllowed: Boolean, - ): LoginEmailBackendResult = - LoginEmailBackendResult.SecondFactorRequired(userIdentifier) - - override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult = - LoginEmailBackendResult.SecondFactorRequired(userIdentifier) - } - - private object InvalidCredentialsBackend : LoginEmailBackend { - override suspend fun login( - userIdentifier: String, - password: String, - secondFactorVerificationCode: String?, - usernameAllowed: Boolean, - ): LoginEmailBackendResult = - LoginEmailBackendResult.Failure(LoginEmailError.InvalidCredentials) - - override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult = - LoginEmailBackendResult.SecondFactorRequired(userIdentifier) - } -} diff --git a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt b/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt deleted file mode 100644 index aa864500b45..00000000000 --- a/shared/export-ios/src/commonTest/kotlin/com/wire/ios/shared/auth/newlogin/NewLoginIdentifierIosViewModelFactoryTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Wire - * Copyright (C) 2026 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.ios.shared.auth.newlogin - -import com.wire.ios.shared.WireIosSharedConfig -import com.wire.ios.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 NewLoginIdentifierIosViewModelFactoryTest { - @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) - } - - @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, - ) - } - - private companion object { - fun newViewModel(): NewLoginIdentifierIosViewModel = - NewLoginIdentifierIosViewModelFactory( - config = WireIosSharedConfig(serverLinks), - backend = LocalNewLoginIdentifierBackend(), - ).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, - ) - } -} diff --git a/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailBackend.kt b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailGateway.kt similarity index 68% rename from shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailBackend.kt rename to shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailGateway.kt index 606107a92d0..df12bd23ae9 100644 --- a/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailBackend.kt +++ b/shared/export-ios/src/iosMain/kotlin/com/wire/ios/shared/auth/email/KaliumLoginEmailGateway.kt @@ -15,12 +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.ios.shared.auth.email +package com.wire.shared.auth.email import com.wire.ios.shared.IosKaliumRuntimeConfig import com.wire.ios.shared.WireIosSharedConfig -import com.wire.ios.shared.auth.login.model.toKalium +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 @@ -35,34 +37,34 @@ 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. + */ @Inject -class KaliumLoginEmailBackend( +class KaliumLoginEmailGateway( private val config: WireIosSharedConfig, private val coreLogic: CoreLogicCommon, -) : LoginEmailBackend { +) : LoginEmailGateway { private val runtimeConfig: IosKaliumRuntimeConfig? get() = config.runtimeConfig - private val localBackend = LocalLoginEmailBackend() - override suspend fun login( userIdentifier: String, password: String, secondFactorVerificationCode: String?, usernameAllowed: Boolean, - ): LoginEmailBackendResult { - val runtime = runtimeConfig ?: return localBackend.login( - userIdentifier = userIdentifier, - password = password, - secondFactorVerificationCode = secondFactorVerificationCode, - usernameAllowed = usernameAllowed, - ) + ): LoginEmailGatewayResult { + val runtime = runtimeConfig ?: return missingRuntimeConfig() if (!usernameAllowed && !coreLogic.getGlobalScope().validateEmailUseCase(userIdentifier)) { - return LoginEmailBackendResult.Failure(LoginEmailError.InvalidUserIdentifier) + return LoginEmailGatewayResult.Failure(LoginEmailError.InvalidUserIdentifier) } val authScope = when (val result = resolveAuthScope(runtime)) { - is AuthScopeResult.Failure -> return LoginEmailBackendResult.Failure(result.error) + is AuthScopeResult.Failure -> return LoginEmailGatewayResult.Failure(result.error) is AuthScopeResult.Success -> result.authScope } val loginResult = authScope.login( @@ -76,12 +78,12 @@ class KaliumLoginEmailBackend( } val storedUserId = addAuthenticatedUser(loginResult) - ?: return LoginEmailBackendResult.Failure(LoginEmailError.UserAlreadyExists) + ?: return LoginEmailGatewayResult.Failure(LoginEmailError.UserAlreadyExists) if (coreLogic.getGlobalScope().validateEmailUseCase(userIdentifier)) { val persistEmailResult = coreLogic.getSessionScope(storedUserId).users.persistSelfUserEmail(userIdentifier) if (persistEmailResult is PersistSelfUserEmailResult.Failure) { - return LoginEmailBackendResult.Failure(LoginEmailError.Generic(persistEmailResult.coreFailure.toString())) + return LoginEmailGatewayResult.Failure(LoginEmailError.Generic(persistEmailResult.coreFailure.toString())) } } @@ -91,23 +93,45 @@ class KaliumLoginEmailBackend( ) ) { is RegisterClientResult.Success -> - LoginEmailBackendResult.Success(initialSyncCompleted = false, isE2EIRequired = false) + LoginEmailGatewayResult.Success( + initialSyncCompleted = false, + isE2EIRequired = false, + payload = loginResult.authData.toSuccessPayload( + runtime = runtime, + userIdentifier = userIdentifier, + password = password, + secondFactorVerificationCode = secondFactorVerificationCode, + isE2EIRequired = false, + clientId = clientResult.client.id.value, + ), + ) is RegisterClientResult.E2EICertificateRequired -> - LoginEmailBackendResult.Success(initialSyncCompleted = false, isE2EIRequired = true) + LoginEmailGatewayResult.Success( + initialSyncCompleted = false, + isE2EIRequired = true, + payload = loginResult.authData.toSuccessPayload( + runtime = runtime, + userIdentifier = userIdentifier, + password = password, + secondFactorVerificationCode = secondFactorVerificationCode, + isE2EIRequired = true, + clientId = clientResult.client.id.value, + ), + ) is RegisterClientResult.Failure.TooManyClients -> - LoginEmailBackendResult.RemoveDeviceNeeded + LoginEmailGatewayResult.RemoveDeviceNeeded is RegisterClientResult.Failure -> - LoginEmailBackendResult.Failure(clientResult.toLoginEmailError()) + LoginEmailGatewayResult.Failure(clientResult.toLoginEmailError()) } } - override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailBackendResult { - val runtime = runtimeConfig ?: return localBackend.requestSecondFactorCode(userIdentifier) + override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailGatewayResult { + val runtime = runtimeConfig ?: return missingRuntimeConfig() val authScope = when (val result = resolveAuthScope(runtime)) { - is AuthScopeResult.Failure -> return LoginEmailBackendResult.Failure(result.error) + is AuthScopeResult.Failure -> return LoginEmailGatewayResult.Failure(result.error) is AuthScopeResult.Success -> result.authScope } return requestSecondFactorCode(authScope, userIdentifier) @@ -141,26 +165,26 @@ class KaliumLoginEmailBackend( failure: AuthenticationResult.Failure, authScope: AuthenticationScope, userIdentifier: String, - ): LoginEmailBackendResult = + ): LoginEmailGatewayResult = when (failure) { AuthenticationResult.Failure.InvalidCredentials.Missing2FA -> requestSecondFactorCode(authScope, userIdentifier) AuthenticationResult.Failure.InvalidCredentials.Invalid2FA -> - LoginEmailBackendResult.SecondFactorRequired( + LoginEmailGatewayResult.SecondFactorRequired( email = userIdentifier, isCurrentCodeInvalid = true, ) - else -> LoginEmailBackendResult.Failure(failure.toLoginEmailError()) + else -> LoginEmailGatewayResult.Failure(failure.toLoginEmailError()) } private suspend fun requestSecondFactorCode( authScope: AuthenticationScope, userIdentifier: String, - ): LoginEmailBackendResult { + ): LoginEmailGatewayResult { if (!userIdentifier.contains("@")) { - return LoginEmailBackendResult.Failure(LoginEmailError.RequestSecondFactorWithHandle) + return LoginEmailGatewayResult.Failure(LoginEmailError.RequestSecondFactorWithHandle) } return when ( val result = authScope.requestSecondFactorVerificationCode( @@ -170,16 +194,40 @@ class KaliumLoginEmailBackend( ) { RequestSecondFactorVerificationCodeUseCase.Result.Success, RequestSecondFactorVerificationCodeUseCase.Result.Failure.TooManyRequests -> - LoginEmailBackendResult.SecondFactorRequired(email = userIdentifier) + LoginEmailGatewayResult.SecondFactorRequired(email = userIdentifier) is RequestSecondFactorVerificationCodeUseCase.Result.Failure.Generic -> - LoginEmailBackendResult.Failure(LoginEmailError.Generic(result.cause.toString())) + LoginEmailGatewayResult.Failure(LoginEmailError.Generic(result.cause.toString())) - else -> LoginEmailBackendResult.Failure(LoginEmailError.Generic()) + else -> LoginEmailGatewayResult.Failure(LoginEmailError.Generic()) } } } +private fun AccountTokens.toSuccessPayload( + runtime: IosKaliumRuntimeConfig, + userIdentifier: String, + password: String, + secondFactorVerificationCode: String?, + isE2EIRequired: Boolean, + clientId: String?, +): AuthLoginSuccessPayload = + AuthLoginSuccessPayload( + userIdValue = userId.value, + userIdDomain = userId.domain.ifBlank { null }, + accessTokenValue = accessToken.value, + accessTokenType = accessToken.tokenType, + accessTokenExpiresInSeconds = null, + refreshTokenValue = refreshToken.value, + refreshTokenCookieDomain = runtime.backendDomain, + email = userIdentifier, + password = password, + secondFactorCode = secondFactorVerificationCode, + initialSyncCompleted = false, + isE2EIRequired = isE2EIRequired, + clientId = clientId, + ) + private sealed interface AuthScopeResult { data class Success(val authScope: AuthenticationScope) : AuthScopeResult data class Failure(val error: LoginEmailError) : AuthScopeResult @@ -211,3 +259,8 @@ private fun RegisterClientResult.Failure.toLoginEmailError(): LoginEmailError = 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 index 47dd6b22183..e46943692f6 100644 --- 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 @@ -15,7 +15,7 @@ * 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.login.model +package com.wire.shared.auth.login.model import com.wire.kalium.logic.configuration.server.ServerConfig 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 index 2f203d1d3ff..918cc262948 100644 --- 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 @@ -15,12 +15,12 @@ * 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 +package com.wire.shared.auth.newlogin import com.wire.ios.shared.IosKaliumRuntimeConfig import com.wire.ios.shared.WireIosSharedConfig -import com.wire.ios.shared.auth.login.model.LoginApiProxy -import com.wire.ios.shared.auth.login.model.LoginServerLinks +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 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 index a23d92b3098..b33c2656817 100644 --- 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 @@ -22,35 +22,45 @@ 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.ios.shared.auth.email.KaliumLoginEmailBackend -import com.wire.ios.shared.auth.email.LoginEmailBackend -import com.wire.ios.shared.auth.email.LoginEmailEffect -import com.wire.ios.shared.auth.email.LoginEmailIntent +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.LoginEmailState import com.wire.ios.shared.auth.email.createGenericLoginEmailIosViewModel import com.wire.ios.shared.auth.email.createLoginEmailIosViewModel -import com.wire.ios.shared.auth.login.model.LoginServerLinks -import com.wire.ios.shared.auth.newlogin.KaliumNewLoginIdentifierBackend -import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierBackend -import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierEffect -import com.wire.ios.shared.auth.newlogin.NewLoginIdentifierIntent +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.NewLoginIdentifierState import com.wire.ios.shared.auth.newlogin.createGenericNewLoginIdentifierIosViewModel import com.wire.ios.shared.auth.newlogin.createNewLoginIdentifierIosViewModel -import com.wire.ios.shared.auth.welcome.WelcomeEffect -import com.wire.ios.shared.auth.welcome.WelcomeIntent +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.WelcomeState 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 @@ -79,6 +89,17 @@ interface WireIosSharedGraph { ) } + @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, @@ -121,9 +142,26 @@ interface WireIosSharedGraph { createGenericLoginEmailIosViewModel(loginEmailIosViewModelFactory) @Provides - fun provideLoginEmailBackend( - backend: KaliumLoginEmailBackend, - ): LoginEmailBackend = backend + 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 @@ -131,6 +169,9 @@ interface WireIosSharedGraph { 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. @@ -151,6 +192,34 @@ fun createWireIosShared(defaultServerLinks: LoginServerLinks): WireIosSharedGrap 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, From 940ad90bb34725ac5305515bac1ae94685b2484e Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Tue, 19 May 2026 16:39:11 +0200 Subject: [PATCH 43/46] chore: bump metro to 1.1.1 --- .../main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt | 2 +- .../com/wire/android/ui/home/appLock/LockCodeTimeManager.kt | 4 +++- gradle/libs.versions.toml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) 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 index ad9cb8b73ea..3c97a70c891 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -975,7 +975,7 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset @Provides fun provideCurrentTimestampProvider(): CurrentTimestampProvider = - { System.currentTimeMillis() } + CurrentTimestampProvider { System.currentTimeMillis() } @Provides fun provideGeocoder(@ApplicationContext context: Context): Geocoder = 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef02efbaced..41daec220ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ firebaseBOM = "34.7.0" fragment = "1.5.6" resaca = "5.0.2" resacaMetro = "5.0.0" -metro = "0.12.1" +metro = "1.1.1" bundlizer = "0.8.0" squareup-javapoet = "1.13.0" visibilityModifiers = "1.1.0" From c3baf8d450e83573520ed5fd5ba1a3cd64fa849b Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 20 May 2026 09:12:35 +0200 Subject: [PATCH 44/46] fix: complete metro wiring after rebase --- .../wire/android/di/metro/WireMetroGraph.kt | 21 +++++++++++ .../promoteadmin/PromoteAdminScreen.kt | 7 +++- .../promoteadmin/PromoteAdminViewModel.kt | 11 +----- .../PromoteAdminViewModelFactory.kt | 37 +++++++++++++++++++ .../search/apps/SearchAppsViewModelFactory.kt | 3 ++ 5 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/home/conversations/promoteadmin/PromoteAdminViewModelFactory.kt 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 index 3c97a70c891..4ef2a8f6aa4 100644 --- a/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt +++ b/app/src/main/kotlin/com/wire/android/di/metro/WireMetroGraph.kt @@ -174,6 +174,7 @@ import com.wire.android.ui.home.conversations.messages.draft.MessageDraftViewMod 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 @@ -455,6 +456,7 @@ 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 @@ -464,6 +466,7 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetails 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 @@ -538,6 +541,7 @@ import com.wire.kalium.logic.feature.service.ObserveAllServicesUseCase import com.wire.kalium.logic.feature.service.ObserveIsServiceMemberUseCase import com.wire.kalium.logic.feature.service.SearchServicesByNameUseCase import com.wire.kalium.logic.feature.service.ServiceScope +import com.wire.kalium.logic.feature.service.SyncServicesUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase @@ -704,6 +708,7 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset val conversationOptionsMenuViewModelFactory: ConversationOptionsMenuViewModelFactory val isFileSharingEnabledViewModelFactory: IsFileSharingEnabledViewModelFactory val addMembersToConversationViewModelFactory: AddMembersToConversationViewModelFactory + val promoteAdminViewModelFactory: PromoteAdminViewModelFactory val editConversationMetadataViewModelFactory: EditConversationMetadataViewModelFactory val searchAppsViewModelFactory: SearchAppsViewModelFactory val recordAudioViewModelFactory: RecordAudioViewModelFactory @@ -2323,6 +2328,18 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset 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 @@ -2488,6 +2505,10 @@ interface WireMetroGraph : CellViewModelGraph, MeetingViewModelGraph, ImageAsset fun provideSearchServicesByNameUseCase(serviceScope: ServiceScope): SearchServicesByNameUseCase = serviceScope.searchServicesByName + @Provides + fun provideSyncServicesUseCase(serviceScope: ServiceScope): SyncServicesUseCase = + serviceScope.syncServices + @Provides fun provideObserveIsAppsAllowedForUsageUseCase(serviceScope: ServiceScope): ObserveIsAppsAllowedForUsageUseCase = serviceScope.observeIsAppsAllowedForUsage 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/apps/SearchAppsViewModelFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/apps/SearchAppsViewModelFactory.kt index f4f23dd80e8..de80911126a 100644 --- 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 @@ -24,12 +24,14 @@ 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, @@ -40,6 +42,7 @@ class SearchAppsViewModelFactory( fun create(protocolInfo: Conversation.ProtocolInfo?): SearchAppsViewModel = SearchAppsViewModel( protocolInfo = protocolInfo, getAllServices = getAllServices, + syncServices = syncServices, getAllApps = getAllApps, contactMapper = contactMapper, searchServicesByName = searchServicesByName, From 5f172ea6885a821b0266a6ab26515f8587ac471b Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 20 May 2026 09:56:35 +0200 Subject: [PATCH 45/46] fix: resolve detekt issues --- .../ConversationOptionsModalSheetLayout.kt | 6 +- .../recordaudio/RecordAudioComponent.kt | 6 +- .../auth/email/LoginEmailViewModelFactory.kt | 246 ++++++++++-------- .../wire/ios/shared/WireIosSharedConfig.kt | 2 + .../ios/shared/auth/bridge/AuthIosBridge.kt | 2 + .../auth/email/KaliumLoginEmailGateway.kt | 220 +++++++++++----- .../ios/shared/export/WireIosSharedGraph.kt | 1 + 7 files changed, 313 insertions(+), 170 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt index 0bd3e2da785..0c963599fb8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt @@ -58,7 +58,11 @@ fun ConversationOptionsModalSheetLayout( onPromoteAdmin: (ConversationId) -> Unit = {}, openConversationDebugMenu: (ConversationId) -> Unit = {}, viewModel: ConversationOptionsMenuViewModel = - wireViewModelScoped() + wireViewModelScoped< + ConversationOptionsMenuViewModelImpl, + ConversationOptionsMenuViewModel, + ConversationOptionsMenuViewModelFactory, + >() ) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current 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 5ca039009d8..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 @@ -51,7 +51,11 @@ fun RecordAudioComponent( onCloseRecordAudio: () -> Unit, modifier: Modifier = Modifier, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - viewModel: RecordAudioViewModel = wireViewModelScoped() + viewModel: RecordAudioViewModel = wireViewModelScoped< + RecordAudioViewModel, + RecordAudioViewModel, + RecordAudioViewModelFactory, + >() ) { val context = LocalContext.current val snackbarHostState = LocalSnackbarHostState.current 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 index d2c563a3280..64cea12840a 100644 --- 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 @@ -52,67 +52,80 @@ class LoginEmailViewModelFactory( return SharedViewModel( state = state.asStateFlow(), effects = effects.asSharedFlow(), - onIntent = { intent -> - when (intent) { - is LoginEmailIntent.UserIdentifierChanged -> - state.update { - it.copy( - userIdentifier = intent.value, - loginEnabled = it.canSubmit(userIdentifier = intent.value), - flowState = LoginEmailFlowState.Default, - ) - } - - is LoginEmailIntent.PasswordChanged -> - state.update { - it.copy( - password = intent.value, - loginEnabled = it.canSubmit(password = intent.value), - flowState = LoginEmailFlowState.Default, - ) - } - - is LoginEmailIntent.ProxyIdentifierChanged -> - state.update { it.copy(proxyIdentifier = intent.value) } - - is LoginEmailIntent.ProxyPasswordChanged -> - state.update { it.copy(proxyPassword = intent.value) } - - is LoginEmailIntent.SecondFactorCodeChanged -> - state.update { - it.copy( - secondFactorVerificationCode = it.secondFactorVerificationCode.copy( - code = intent.value, - isCurrentCodeInvalid = false, - ) - ) - } - - 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 -> - state.update { - it.copy( - secondFactorVerificationCode = LoginEmailVerificationCodeState(), - flowState = LoginEmailFlowState.Default, - ) - } - - LoginEmailIntent.ResendSecondFactorCode -> - requestSecondFactorCode(state, scope) - } - }, + 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, @@ -126,66 +139,83 @@ class LoginEmailViewModelFactory( } state.update { it.copy(flowState = LoginEmailFlowState.Loading, loginEnabled = false) } scope.launch { - when ( - val result = gateway.login( + handleLoginResult( + result = gateway.login( userIdentifier = currentState.userIdentifier, password = currentState.password, secondFactorVerificationCode = currentState.secondFactorVerificationCode.code.takeIf { it.isNotBlank() }, usernameAllowed = usernameAllowed, - ) - ) { - is LoginEmailGatewayResult.Failure -> { - state.update { - val flowState = LoginEmailFlowState.Error(result.error) - it.copy( - flowState = flowState, - loginEnabled = it.canSubmit(flowState = flowState), - ) - } - } - - LoginEmailGatewayResult.RemoveDeviceNeeded -> { - effects.tryEmit(LoginEmailEffect.RemoveDeviceNeeded) - state.update { - val flowState = LoginEmailFlowState.Error(LoginEmailError.TooManyDevices) - it.copy( - flowState = flowState, - loginEnabled = it.canSubmit(flowState = flowState), - ) - } - } + ), + state = state, + effects = effects, + ) + } + } - is 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, - ), - ) - } - } - - is 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 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, 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 index d93720b2e0d..defda5a4194 100644 --- 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 @@ -58,6 +58,7 @@ fun createWireIosSharedConfig( runtimeConfig = runtimeConfig, ) +@Suppress("LongParameterList") fun createWireIosSharedConfig( defaultServerLinks: LoginServerLinks, runtimeConfig: IosKaliumRuntimeConfig?, @@ -92,6 +93,7 @@ fun createIosKaliumRuntimeConfig( migrationMode = migrationMode, ) +@Suppress("LongParameterList") fun createIosKaliumRuntimeConfig( appGroupRootPath: String, accountDataPath: String?, 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 index bf82a9c4054..5373bc65f6c 100644 --- 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 @@ -15,6 +15,8 @@ * 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 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 index df12bd23ae9..b9a0189e90b 100644 --- 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 @@ -32,7 +32,6 @@ 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.GetOrRegisterClientUseCase import com.wire.kalium.logic.feature.client.RegisterClientParam import com.wire.kalium.logic.feature.client.RegisterClientResult import dev.zacsweers.metro.Inject @@ -44,6 +43,7 @@ import dev.zacsweers.metro.Inject * 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, @@ -57,52 +57,133 @@ class KaliumLoginEmailGateway( password: String, secondFactorVerificationCode: String?, usernameAllowed: Boolean, - ): LoginEmailGatewayResult { - val runtime = runtimeConfig ?: return missingRuntimeConfig() + ): LoginEmailGatewayResult = withRuntimeConfig { runtime -> if (!usernameAllowed && !coreLogic.getGlobalScope().validateEmailUseCase(userIdentifier)) { - return LoginEmailGatewayResult.Failure(LoginEmailError.InvalidUserIdentifier) + LoginEmailGatewayResult.Failure(LoginEmailError.InvalidUserIdentifier) + } else { + loginWithRuntime( + runtime = runtime, + userIdentifier = userIdentifier, + password = password, + secondFactorVerificationCode = secondFactorVerificationCode, + ) } + } - val authScope = when (val result = resolveAuthScope(runtime)) { - is AuthScopeResult.Failure -> return LoginEmailGatewayResult.Failure(result.error) - is AuthScopeResult.Success -> result.authScope + 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) + } } - val loginResult = authScope.login( - userIdentifier = userIdentifier, - password = password, - shouldPersistClient = true, - secondFactorVerificationCode = secondFactorVerificationCode, - ) - if (loginResult !is AuthenticationResult.Success) { - return handleAuthenticationFailure(loginResult as AuthenticationResult.Failure, 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, + ) } - val storedUserId = addAuthenticatedUser(loginResult) - ?: return LoginEmailGatewayResult.Failure(LoginEmailError.UserAlreadyExists) + 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 LoginEmailGatewayResult.Failure(LoginEmailError.Generic(persistEmailResult.coreFailure.toString())) + return LoginEmailError.Generic(persistEmailResult.coreFailure.toString()) } } + return null + } - return when ( + private suspend fun registerClient( + input: AuthLoginAttemptInput, + storedUserId: UserId, + ): LoginEmailGatewayResult = + when ( val clientResult = coreLogic.getSessionScope(storedUserId).client.getOrRegister( - RegisterClientParam(password = password, capabilities = null) + RegisterClientParam(password = input.password, capabilities = null) ) ) { is RegisterClientResult.Success -> LoginEmailGatewayResult.Success( initialSyncCompleted = false, isE2EIRequired = false, - payload = loginResult.authData.toSuccessPayload( - runtime = runtime, - userIdentifier = userIdentifier, - password = password, - secondFactorVerificationCode = secondFactorVerificationCode, - isE2EIRequired = false, - clientId = clientResult.client.id.value, + payload = input.loginResult.authData.toSuccessPayload( + input = input.toSuccessPayloadInput( + isE2EIRequired = false, + clientId = clientResult.client.id.value, + ), ), ) @@ -110,13 +191,11 @@ class KaliumLoginEmailGateway( LoginEmailGatewayResult.Success( initialSyncCompleted = false, isE2EIRequired = true, - payload = loginResult.authData.toSuccessPayload( - runtime = runtime, - userIdentifier = userIdentifier, - password = password, - secondFactorVerificationCode = secondFactorVerificationCode, - isE2EIRequired = true, - clientId = clientResult.client.id.value, + payload = input.loginResult.authData.toSuccessPayload( + input = input.toSuccessPayloadInput( + isE2EIRequired = true, + clientId = clientResult.client.id.value, + ), ), ) @@ -126,16 +205,6 @@ class KaliumLoginEmailGateway( is RegisterClientResult.Failure -> LoginEmailGatewayResult.Failure(clientResult.toLoginEmailError()) } - } - - override suspend fun requestSecondFactorCode(userIdentifier: String): LoginEmailGatewayResult { - val runtime = runtimeConfig ?: return missingRuntimeConfig() - val authScope = when (val result = resolveAuthScope(runtime)) { - is AuthScopeResult.Failure -> return LoginEmailGatewayResult.Failure(result.error) - is AuthScopeResult.Success -> result.authScope - } - return requestSecondFactorCode(authScope, userIdentifier) - } private suspend fun resolveAuthScope(runtime: IosKaliumRuntimeConfig): AuthScopeResult = when (val result = coreLogic.versionedAuthenticationScope(runtime.serverLinks.toKalium()).invoke(null)) { @@ -182,11 +251,18 @@ class KaliumLoginEmailGateway( private suspend fun requestSecondFactorCode( authScope: AuthenticationScope, userIdentifier: String, - ): LoginEmailGatewayResult { - if (!userIdentifier.contains("@")) { - return LoginEmailGatewayResult.Failure(LoginEmailError.RequestSecondFactorWithHandle) + ): LoginEmailGatewayResult = + if (userIdentifier.contains("@")) { + requestSecondFactorCodeForEmail(authScope, userIdentifier) + } else { + LoginEmailGatewayResult.Failure(LoginEmailError.RequestSecondFactorWithHandle) } - return when ( + + private suspend fun requestSecondFactorCodeForEmail( + authScope: AuthenticationScope, + userIdentifier: String, + ): LoginEmailGatewayResult = + when ( val result = authScope.requestSecondFactorVerificationCode( email = userIdentifier, verifiableAction = VerifiableAction.LOGIN_OR_CLIENT_REGISTRATION, @@ -201,16 +277,40 @@ class KaliumLoginEmailGateway( else -> LoginEmailGatewayResult.Failure(LoginEmailError.Generic()) } - } } -private fun AccountTokens.toSuccessPayload( - runtime: IosKaliumRuntimeConfig, - userIdentifier: String, - password: String, - secondFactorVerificationCode: String?, +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, @@ -219,13 +319,13 @@ private fun AccountTokens.toSuccessPayload( accessTokenType = accessToken.tokenType, accessTokenExpiresInSeconds = null, refreshTokenValue = refreshToken.value, - refreshTokenCookieDomain = runtime.backendDomain, - email = userIdentifier, - password = password, - secondFactorCode = secondFactorVerificationCode, + refreshTokenCookieDomain = input.runtime.backendDomain, + email = input.userIdentifier, + password = input.password, + secondFactorCode = input.secondFactorVerificationCode, initialSyncCompleted = false, - isE2EIRequired = isE2EIRequired, - clientId = clientId, + isE2EIRequired = input.isE2EIRequired, + clientId = input.clientId, ) private sealed interface AuthScopeResult { 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 index b33c2656817..a4002929c2d 100644 --- 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 @@ -66,6 +66,7 @@ import dev.zacsweers.metro.Provides import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.createGraphFactory +@Suppress("TooManyFunctions") @DependencyGraph(WireIosSharedScope::class) interface WireIosSharedGraph { @DependencyGraph.Factory From 1a203b8f7e4bde190a928e30557558820ff5d425 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Wed, 20 May 2026 11:47:28 +0200 Subject: [PATCH 46/46] fix: repair test coverage task --- app/build.gradle.kts | 2 +- .../promoteadmin/PromoteAdminViewModelTest.kt | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 84f7dcfd07f..155dbb2d483 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -201,7 +201,7 @@ dependencies { implementationWithCoverage(projects.features.sketch) implementationWithCoverage(projects.features.meetings) implementationWithCoverage(projects.features.sync) - implementationWithCoverage(projects.shared.auth) + implementation(projects.shared.auth) // Anonymous Analytics val flavors = getFlavorsSettings() 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")), ) } }