From 733173605be951bece7dcda0194debef79ab6fc2 Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 22 Dec 2025 14:51:48 +0100 Subject: [PATCH 01/10] feat: create file in a cells conversation --- .../android/navigation/WireMainNavGraph.kt | 2 + .../cells/ui/ConversationFilesScreen.kt | 19 ++ .../ui/create/FileTypeBottomSheetDialog.kt | 106 ++++++++++ .../ui/create/createfile/CreateFileScreen.kt | 184 ++++++++++++++++++ .../createfile/CreateFileScreenNavArgs.kt | 22 +++ .../create/createfile/CreateFileViewModel.kt | 73 +++++++ .../createfolder/CreateFolderScreen.kt | 8 +- .../createfolder/CreateFolderScreenNavArgs.kt | 2 +- .../createfolder/CreateFolderViewModel.kt | 6 +- .../ui/dialog/CellsNewActionBottomSheet.kt | 32 ++- .../cells/ui/rename/RenameNodeScreen.kt | 6 +- .../cells/src/main/res/values-de/strings.xml | 2 +- .../cells/src/main/res/values-ru/strings.xml | 2 +- .../cells/src/main/res/values/strings.xml | 11 +- 14 files changed, 459 insertions(+), 16 deletions(-) create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt rename features/cells/src/main/java/com/wire/android/feature/cells/ui/{ => create}/createfolder/CreateFolderScreen.kt (96%) rename features/cells/src/main/java/com/wire/android/feature/cells/ui/{ => create}/createfolder/CreateFolderScreenNavArgs.kt (92%) rename features/cells/src/main/java/com/wire/android/feature/cells/ui/{ => create}/createfolder/CreateFolderViewModel.kt (93%) diff --git a/app/src/main/kotlin/com/wire/android/navigation/WireMainNavGraph.kt b/app/src/main/kotlin/com/wire/android/navigation/WireMainNavGraph.kt index 783477b35c3..3b63b8c18e8 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/WireMainNavGraph.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/WireMainNavGraph.kt @@ -23,6 +23,7 @@ import com.ramcosta.composedestinations.spec.NavGraphSpec import com.wire.android.feature.cells.ui.destinations.AddRemoveTagsScreenDestination import com.wire.android.feature.cells.ui.destinations.ConversationFilesScreenDestination import com.wire.android.feature.cells.ui.destinations.ConversationFilesWithSlideInTransitionScreenDestination +import com.wire.android.feature.cells.ui.destinations.CreateFileScreenDestination import com.wire.android.feature.cells.ui.destinations.CreateFolderScreenDestination import com.wire.android.feature.cells.ui.destinations.MoveToFolderScreenDestination import com.wire.android.feature.cells.ui.destinations.PublicLinkExpirationScreenDestination @@ -43,6 +44,7 @@ object WireMainNavGraph : NavGraphSpec { .plus(ConversationFilesScreenDestination) .plus(ConversationFilesWithSlideInTransitionScreenDestination) .plus(CreateFolderScreenDestination) + .plus(CreateFileScreenDestination) .plus(MoveToFolderScreenDestination) .plus(RecycleBinScreenDestination) .plus(RenameNodeScreenDestination) 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 fd3f18835b6..7aef0e62889 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 @@ -52,8 +52,11 @@ import androidx.paging.compose.collectAsLazyPagingItems import com.wire.android.feature.cells.R import com.wire.android.feature.cells.domain.model.AttachmentFileType import com.wire.android.feature.cells.ui.common.Breadcrumbs +import com.wire.android.feature.cells.ui.create.FileTypeBottomSheetDialog +import com.wire.android.feature.cells.ui.create.createfile.CreateFileScreenNavArgs import com.wire.android.feature.cells.ui.destinations.AddRemoveTagsScreenDestination import com.wire.android.feature.cells.ui.destinations.ConversationFilesWithSlideInTransitionScreenDestination +import com.wire.android.feature.cells.ui.destinations.CreateFileScreenDestination import com.wire.android.feature.cells.ui.destinations.CreateFolderScreenDestination import com.wire.android.feature.cells.ui.destinations.MoveToFolderScreenDestination import com.wire.android.feature.cells.ui.destinations.PublicLinkScreenDestination @@ -159,6 +162,7 @@ fun ConversationFilesScreenContent( breadcrumbs: Array? = emptyArray(), ) { val newActionBottomSheetState = rememberWireModalSheetState() + val fileTypeBottomSheetState = rememberWireModalSheetState() val optionsBottomSheetState = rememberWireModalSheetState() val isFabVisible = when { @@ -176,6 +180,10 @@ fun ConversationFilesScreenContent( onCreateFolder = { newActionBottomSheetState.hide() navigator.navigate(NavigationCommand(CreateFolderScreenDestination(currentNodeUuid))) + }, + onCreateFile = { + newActionBottomSheetState.hide() + fileTypeBottomSheetState.show() } ) @@ -198,6 +206,17 @@ fun ConversationFilesScreenContent( } ) + FileTypeBottomSheetDialog( + sheetState = fileTypeBottomSheetState, + onDismiss = { + fileTypeBottomSheetState.hide() + }, + onItemSelected = { + navigator.navigate(NavigationCommand(CreateFileScreenDestination(CreateFileScreenNavArgs(it)))) + fileTypeBottomSheetState.hide() + }, + ) + CollapsingTopBarScaffold( modifier = modifier, topBarHeader = { diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt new file mode 100644 index 00000000000..1fdece47691 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt @@ -0,0 +1,106 @@ +/* + * 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.feature.cells.ui.create + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.wire.android.feature.cells.R +import com.wire.android.ui.common.bottomsheet.MenuBottomSheetItem +import com.wire.android.ui.common.bottomsheet.WireMenuModalSheetContent +import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout +import com.wire.android.ui.common.bottomsheet.WireModalSheetState +import com.wire.android.ui.common.bottomsheet.WireSheetValue +import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireDimensions + +@Composable +fun FileTypeBottomSheetDialog( + sheetState: WireModalSheetState, + onDismiss: () -> Unit, + onItemSelected: (String) -> Unit, +) { + WireModalSheetLayout( + onDismissRequest = onDismiss, + sheetState = sheetState + ) { + WireMenuModalSheetContent( + menuItems = buildList { + add { + BottomSheetItem( + title = stringResource(R.string.file_type_bottom_sheet_document), + icon = R.drawable.ic_file_type_doc, + onClicked = { onItemSelected(".docx") }, + ) + } + add { + BottomSheetItem( + title = stringResource(R.string.file_type_bottom_sheet_spreadsheet), + icon = R.drawable.ic_file_type_spreadsheet, + onClicked = { onItemSelected(".xlsx") }, + ) + } + add { + BottomSheetItem( + title = stringResource(R.string.file_type_bottom_sheet_presentation), + icon = R.drawable.ic_file_type_presentation, + onClicked = { onItemSelected(".pptx") }, + ) + } + } + ) + } +} + +@Composable +private fun BottomSheetItem( + title: String, + icon: Int = R.drawable.ic_folder, + onClicked: () -> Unit, +) { + MenuBottomSheetItem( + title = title, + onItemClick = onClicked, + leading = { + Image( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier + .size(MaterialTheme.wireDimensions.wireIconButtonSize) + ) + }, + ) +} + +@MultipleThemePreviews +@Composable +fun PreviewFileTypeBottomSheetDialog() { + WireTheme { + FileTypeBottomSheetDialog( + sheetState = rememberWireModalSheetState(WireSheetValue.Expanded(value = Unit)), + onDismiss = {}, + onItemSelected = {}, + ) + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt new file mode 100644 index 00000000000..126753a90c0 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt @@ -0,0 +1,184 @@ +/* + * 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.feature.cells.ui.create.createfile + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +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.ramcosta.composedestinations.result.ResultBackNavigator +import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.common.FileNameError +import com.wire.android.navigation.PreviewNavigator +import com.wire.android.navigation.PreviewResultBackNavigator +import com.wire.android.navigation.WireNavigator +import com.wire.android.navigation.annotation.features.cells.WireDestination +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.HandleActions +import com.wire.android.ui.common.WireDialog +import com.wire.android.ui.common.WireDialogButtonProperties +import com.wire.android.ui.common.WireDialogButtonType +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.textfield.WireTextField +import com.wire.android.ui.common.textfield.WireTextFieldState +import com.wire.android.ui.common.topappbar.NavigationIconType +import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireColorScheme +import com.wire.android.ui.theme.wireDimensions +import java.util.Locale + +@WireDestination( + style = PopUpNavigationAnimation::class, + navArgsDelegate = CreateFileScreenNavArgs::class, +) +@Composable +fun CreateFileScreen( + navigator: WireNavigator, + resultNavigator: ResultBackNavigator, + modifier: Modifier = Modifier, + createFileViewModel: CreateFileViewModel = hiltViewModel() +) { + val showErrorDialog = remember { mutableStateOf(false) } + + if (showErrorDialog.value) { + WireDialog( + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false, usePlatformDefaultWidth = false), + title = stringResource(id = R.string.cells_create_file), + text = stringResource(id = R.string.create_file_error), + onDismiss = { showErrorDialog.value = false }, + dismissButtonProperties = WireDialogButtonProperties( + onClick = { showErrorDialog.value = false }, + text = stringResource(id = R.string.cancel), + type = WireDialogButtonType.Secondary, + ) + ) + } + + WireScaffold( + modifier = modifier, + topBar = { + WireCenterAlignedTopAppBar( + onNavigationPressed = { navigator.navigateBack() }, + navigationIconType = NavigationIconType.Close(), + elevation = dimensions().spacing0x, + title = stringResource(id = R.string.create_file_screen_title, createFileViewModel.fileExtension), + ) + }, + bottomBar = { + AnimatedVisibility( + visible = true, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut(), + ) { + Surface( + color = MaterialTheme.wireColorScheme.background, + shadowElevation = MaterialTheme.wireDimensions.bottomNavigationShadowElevation + ) { + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .padding(dimensions().spacing16x) + ) { + with(createFileViewModel) { + WirePrimaryButton( + text = stringResource(R.string.cells_create_file), + onClick = { + // TODO create file + }, + state = if (viewState.saveEnabled && !viewState.loading) { + WireButtonState.Default + } else { + WireButtonState.Disabled + }, + loading = viewState.loading + ) + } + } + } + } + } + ) { + WireTextField( + textState = createFileViewModel.fileNameTextFieldState, + placeholderText = stringResource(R.string.cells_folder_name), + labelText = stringResource(R.string.cells_folder_name).uppercase(Locale.getDefault()), + modifier = Modifier + .padding(it) + .padding( + top = dimensions().spacing32x, + start = dimensions().spacing16x, + end = dimensions().spacing16x + ), + state = computeNameErrorState(createFileViewModel.viewState.error), + ) + } + + HandleActions(createFileViewModel.actions) { action -> + when (action) { + CreateFileViewModelAction.Success -> { + resultNavigator.setResult(true) + resultNavigator.navigateBack() + } + CreateFileViewModelAction.Failure -> { + showErrorDialog.value = true + } + } + } +} + +@Composable +private fun computeNameErrorState(error: FileNameError?): WireTextFieldState { + val messageRes = when (error) { + FileNameError.NameEmpty -> R.string.cell_file_name + FileNameError.NameExceedLimit -> R.string.long_file_name_error + FileNameError.NameAlreadyExist -> R.string.rename_file_already_exist + FileNameError.InvalidName -> R.string.rename_invalid_name + null -> return WireTextFieldState.Default + } + + return WireTextFieldState.Error(stringResource(id = messageRes)) +} + +@MultipleThemePreviews +@Composable +fun PreviewCreateFileScreen() { + WireTheme { + CreateFileScreen( + navigator = PreviewNavigator, + resultNavigator = PreviewResultBackNavigator as ResultBackNavigator, + ) + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt new file mode 100644 index 00000000000..5d8b85b4f30 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt @@ -0,0 +1,22 @@ +/* + * 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.feature.cells.ui.create.createfile + +data class CreateFileScreenNavArgs( + val extension: String, +) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt new file mode 100644 index 00000000000..b2091901a19 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt @@ -0,0 +1,73 @@ +/* + * 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.feature.cells.ui.create.createfile + +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.wire.android.feature.cells.ui.common.FileNameError +import com.wire.android.feature.cells.ui.common.validateFileName +import com.wire.android.feature.cells.ui.navArgs +import com.wire.android.ui.common.ActionsViewModel +import com.wire.android.ui.common.textfield.textAsFlow +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( + val savedStateHandle: SavedStateHandle, +) : ActionsViewModel() { + + private val navArgs: CreateFileScreenNavArgs = savedStateHandle.navArgs() + + val fileExtension: String = navArgs.extension + + val fileNameTextFieldState: TextFieldState = TextFieldState() + + var viewState: CreateFolderViewState by mutableStateOf(CreateFolderViewState()) + private set + + init { + viewModelScope.launch { + fileNameTextFieldState.textAsFlow().map { it.trim() }.collectLatest { name -> + val fileValidationResult = name.validateFileName().takeIf { name.isNotEmpty() } + viewState = viewState.copy( + saveEnabled = fileValidationResult == null && name.isNotEmpty(), + error = fileValidationResult + ) + } + } + } +} + +sealed interface CreateFileViewModelAction { + data object Success : CreateFileViewModelAction + data object Failure : CreateFileViewModelAction +} + +data class CreateFolderViewState( + val loading: Boolean = false, + val saveEnabled: Boolean = false, + val error: FileNameError? = null, +) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/createfolder/CreateFolderScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreen.kt similarity index 96% rename from features/cells/src/main/java/com/wire/android/feature/cells/ui/createfolder/CreateFolderScreen.kt rename to features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreen.kt index 84209ac8e1e..91638762837 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/createfolder/CreateFolderScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreen.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.android.feature.cells.ui.createfolder +package com.wire.android.feature.cells.ui.create.createfolder import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically @@ -117,7 +117,7 @@ fun CreateFolderScreen( text = stringResource(R.string.cells_create_folder), onClick = { createFolder( - folderName = fileNameTextFieldState.text.toString() + folderName = folderNameTextFieldState.text.toString() ) }, state = if (viewState.saveEnabled && !viewState.loading) { @@ -134,7 +134,7 @@ fun CreateFolderScreen( } ) { WireTextField( - textState = createFolderViewModel.fileNameTextFieldState, + textState = createFolderViewModel.folderNameTextFieldState, placeholderText = stringResource(R.string.cells_folder_name), labelText = stringResource(R.string.cells_folder_name).uppercase(Locale.getDefault()), modifier = Modifier @@ -166,7 +166,7 @@ private fun computeNameErrorState(error: FileNameError?): WireTextFieldState { val messageRes = when (error) { FileNameError.NameEmpty -> R.string.cells_folder_name FileNameError.NameExceedLimit -> R.string.rename_long_folder_name_error - FileNameError.NameAlreadyExist -> R.string.rename_already_exist + FileNameError.NameAlreadyExist -> R.string.rename_folder_already_exist FileNameError.InvalidName -> R.string.rename_invalid_name null -> return WireTextFieldState.Default } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/createfolder/CreateFolderScreenNavArgs.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreenNavArgs.kt similarity index 92% rename from features/cells/src/main/java/com/wire/android/feature/cells/ui/createfolder/CreateFolderScreenNavArgs.kt rename to features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreenNavArgs.kt index 4725131fead..d0d385d291f 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/createfolder/CreateFolderScreenNavArgs.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreenNavArgs.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.android.feature.cells.ui.createfolder +package com.wire.android.feature.cells.ui.create.createfolder data class CreateFolderScreenNavArgs( val uuid: String? diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/createfolder/CreateFolderViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderViewModel.kt similarity index 93% rename from features/cells/src/main/java/com/wire/android/feature/cells/ui/createfolder/CreateFolderViewModel.kt rename to features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderViewModel.kt index 15cf11af971..c3fdd7fe185 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/createfolder/CreateFolderViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderViewModel.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.android.feature.cells.ui.createfolder +package com.wire.android.feature.cells.ui.create.createfolder import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue @@ -45,14 +45,14 @@ class CreateFolderViewModel @Inject constructor( private val navArgs: CreateFolderScreenNavArgs = savedStateHandle.navArgs() - val fileNameTextFieldState: TextFieldState = TextFieldState() + val folderNameTextFieldState: TextFieldState = TextFieldState() var viewState: CreateFolderViewState by mutableStateOf(CreateFolderViewState()) private set init { viewModelScope.launch { - fileNameTextFieldState.textAsFlow().map { it.trim() }.collectLatest { name -> + folderNameTextFieldState.textAsFlow().map { it.trim() }.collectLatest { name -> val fileValidationResult = name.validateFileName().takeIf { name.isNotEmpty() } viewState = viewState.copy( saveEnabled = fileValidationResult == null && name.isNotEmpty(), diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/dialog/CellsNewActionBottomSheet.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/dialog/CellsNewActionBottomSheet.kt index 7da774053d6..f835e435631 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/dialog/CellsNewActionBottomSheet.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/dialog/CellsNewActionBottomSheet.kt @@ -40,6 +40,7 @@ internal fun CellsNewActionBottomSheet( sheetState: WireModalSheetState, onDismiss: () -> Unit, onCreateFolder: () -> Unit, + onCreateFile: () -> Unit, ) { WireModalSheetLayout( onDismissRequest = onDismiss, @@ -51,7 +52,12 @@ internal fun CellsNewActionBottomSheet( CreateFolderSheetItem( title = stringResource(R.string.cells_create_folder), onClicked = onCreateFolder, - enabled = true + ) + } + add { + CreateFileSheetItem( + title = stringResource(R.string.cells_create_file), + onClicked = onCreateFile, ) } } @@ -63,7 +69,6 @@ internal fun CellsNewActionBottomSheet( private fun CreateFolderSheetItem( title: String, onClicked: () -> Unit, - enabled: Boolean, ) { MenuBottomSheetItem( title = title, @@ -76,7 +81,25 @@ private fun CreateFolderSheetItem( .size(MaterialTheme.wireDimensions.wireIconButtonSize) ) }, - enabled = enabled + ) +} + +@Composable +private fun CreateFileSheetItem( + title: String, + onClicked: () -> Unit, +) { + MenuBottomSheetItem( + title = title, + onItemClick = onClicked, + leading = { + Icon( + painter = painterResource(id = com.wire.android.ui.common.R.drawable.ic_plus), + contentDescription = null, + modifier = Modifier + .size(MaterialTheme.wireDimensions.wireIconButtonSize) + ) + }, ) } @@ -87,7 +110,8 @@ private fun PreviewFilesNewActionsBottomSheet() { CellsNewActionBottomSheet( sheetState = rememberWireModalSheetState(WireSheetValue.Expanded(value = Unit)), onDismiss = {}, - onCreateFolder = {} + onCreateFolder = {}, + onCreateFile = {} ) } } 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 c0b53e8b4db..a1a497e3d4f 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 @@ -151,9 +151,13 @@ private fun computeNameErrorState(error: FileNameError?, isFolder: Boolean): Wir val messageRes = when (error) { FileNameError.NameEmpty -> if (isFolder) R.string.rename_enter_folder_name else R.string.rename_enter_file_name + FileNameError.NameExceedLimit -> if (isFolder) R.string.rename_long_folder_name_error else R.string.rename_long_file_name_error - FileNameError.NameAlreadyExist -> R.string.rename_already_exist + + FileNameError.NameAlreadyExist -> + if (isFolder) R.string.rename_folder_already_exist else R.string.rename_file_already_exist + FileNameError.InvalidName -> R.string.rename_invalid_name null -> return WireTextFieldState.Default } diff --git a/features/cells/src/main/res/values-de/strings.xml b/features/cells/src/main/res/values-de/strings.xml index 8b270257f65..f21f1f7b465 100644 --- a/features/cells/src/main/res/values-de/strings.xml +++ b/features/cells/src/main/res/values-de/strings.xml @@ -100,7 +100,7 @@ Datei wurde umbenannt Ordner wurde umbenannt Umbenennen nicht möglich - Eine Datei mit diesem Namen ist bereits vorhanden + Eine Datei mit diesem Namen ist bereits vorhanden Filter Tags Tags auswählen diff --git a/features/cells/src/main/res/values-ru/strings.xml b/features/cells/src/main/res/values-ru/strings.xml index 44f7aaa4482..c3d464c70e6 100644 --- a/features/cells/src/main/res/values-ru/strings.xml +++ b/features/cells/src/main/res/values-ru/strings.xml @@ -109,7 +109,7 @@ Папка переименована Не удается переименовать Используйте название без \"/\" и не начинающееся с \".\" - Файл с таким названием уже существует + Файл с таким названием уже существует Фильтр Теги выбрать теги diff --git a/features/cells/src/main/res/values/strings.xml b/features/cells/src/main/res/values/strings.xml index 369c99457cc..900864b12b5 100644 --- a/features/cells/src/main/res/values/strings.xml +++ b/features/cells/src/main/res/values/strings.xml @@ -108,7 +108,8 @@ Folder was renamed Unable to rename Use a name without "/" and not starting with "." - File with this name already exists + File with this name already exists + Folder with this name already exists Filter Tags select tags @@ -195,4 +196,12 @@ Show Downloading… Search files or folders + Create File + Unable to create file. Please try again + File Name + Use a shorter file name (at most 64 characters) + Document + Spreadsheet + Presentation + Create %1$s file From 9f43fe4bd0690c6cf4759a085c702ada3be12715 Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 22 Dec 2025 17:58:07 +0100 Subject: [PATCH 02/10] feat: invoke use case from viewModel to create files --- .../android/di/accountScoped/CellsModule.kt | 21 +++++++-- .../MultipartAttachmentsViewModel.kt | 2 +- .../MultipartAttachmentsViewModelTest.kt | 2 +- .../android/feature/cells/ui/CellViewModel.kt | 2 +- .../cells/ui/ConversationFilesScreen.kt | 4 +- .../ui/create/FileTypeBottomSheetDialog.kt | 9 ++-- .../ui/create/createfile/CreateFileScreen.kt | 6 +-- .../createfile/CreateFileScreenNavArgs.kt | 3 +- .../create/createfile/CreateFileViewModel.kt | 45 ++++++++++++++++++- .../cells/ui/create/createfile/FileType.kt | 22 +++++++++ .../createfolder/CreateFolderViewModel.kt | 2 +- .../ui/versioning/VersionHistoryViewModel.kt | 2 +- .../feature/cells/ui/CellViewModelTest.kt | 2 +- .../versioning/VersionHistoryViewModelTest.kt | 2 +- kalium | 2 +- 15 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/FileType.kt 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 356266070ce..44d4b95b2e0 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 @@ -24,10 +24,7 @@ import com.wire.android.ui.home.conversations.model.messagetypes.multipart.CellA 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.CreateFolderUseCase import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase -import com.wire.kalium.cells.domain.usecase.DownloadCellFileUseCase -import com.wire.kalium.cells.domain.usecase.DownloadCellVersionUseCase import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase import com.wire.kalium.cells.domain.usecase.GetCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase @@ -47,6 +44,12 @@ 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 @@ -142,6 +145,18 @@ class CellsModule { @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 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 fbc075b71af..936389ef7de 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 @@ -29,7 +29,7 @@ import com.wire.android.feature.cells.ui.edit.OnlineEditor import com.wire.android.ui.common.multipart.MultipartAttachmentUi import com.wire.android.ui.common.multipart.toUiModel import com.wire.android.util.FileManager -import com.wire.kalium.cells.domain.usecase.DownloadCellFileUseCase +import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase import com.wire.kalium.common.functional.onSuccess import com.wire.kalium.logic.data.asset.AssetTransferStatus diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModelTest.kt index ab814499f7a..3045f06b856 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/model/messagetypes/multipart/MultipartAttachmentsViewModelTest.kt @@ -23,7 +23,7 @@ import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.ui.common.multipart.AssetSource import com.wire.android.ui.common.multipart.MultipartAttachmentUi import com.wire.android.util.FileManager -import com.wire.kalium.cells.domain.usecase.DownloadCellFileUseCase +import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase import com.wire.kalium.common.functional.right import com.wire.kalium.logic.data.asset.AssetTransferStatus 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 346eb61eaa1..49c01e59a02 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 @@ -39,7 +39,7 @@ import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.common.DEFAULT_SEARCH_QUERY_DEBOUNCE import com.wire.kalium.cells.domain.model.Node import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase -import com.wire.kalium.cells.domain.usecase.DownloadCellFileUseCase +import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase 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 7aef0e62889..138898125e9 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 @@ -212,7 +212,9 @@ fun ConversationFilesScreenContent( fileTypeBottomSheetState.hide() }, onItemSelected = { - navigator.navigate(NavigationCommand(CreateFileScreenDestination(CreateFileScreenNavArgs(it)))) + currentNodeUuid?.let { uuid -> + navigator.navigate(NavigationCommand(CreateFileScreenDestination(CreateFileScreenNavArgs(uuid, it)))) + } fileTypeBottomSheetState.hide() }, ) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt index 1fdece47691..60ba8b40a5b 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.create.createfile.FileType import com.wire.android.ui.common.bottomsheet.MenuBottomSheetItem import com.wire.android.ui.common.bottomsheet.WireMenuModalSheetContent import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout @@ -39,7 +40,7 @@ import com.wire.android.ui.theme.wireDimensions fun FileTypeBottomSheetDialog( sheetState: WireModalSheetState, onDismiss: () -> Unit, - onItemSelected: (String) -> Unit, + onItemSelected: (FileType) -> Unit, ) { WireModalSheetLayout( onDismissRequest = onDismiss, @@ -51,21 +52,21 @@ fun FileTypeBottomSheetDialog( BottomSheetItem( title = stringResource(R.string.file_type_bottom_sheet_document), icon = R.drawable.ic_file_type_doc, - onClicked = { onItemSelected(".docx") }, + onClicked = { onItemSelected(FileType.DOCUMENT) }, ) } add { BottomSheetItem( title = stringResource(R.string.file_type_bottom_sheet_spreadsheet), icon = R.drawable.ic_file_type_spreadsheet, - onClicked = { onItemSelected(".xlsx") }, + onClicked = { onItemSelected(FileType.SPREADSHEET) }, ) } add { BottomSheetItem( title = stringResource(R.string.file_type_bottom_sheet_presentation), icon = R.drawable.ic_file_type_presentation, - onClicked = { onItemSelected(".pptx") }, + onClicked = { onItemSelected(FileType.PRESENTATION) }, ) } } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt index 126753a90c0..094de341500 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt @@ -116,7 +116,7 @@ fun CreateFileScreen( WirePrimaryButton( text = stringResource(R.string.cells_create_file), onClick = { - // TODO create file + createFileViewModel.createFile(fileNameTextFieldState.text.toString()) }, state = if (viewState.saveEnabled && !viewState.loading) { WireButtonState.Default @@ -133,8 +133,8 @@ fun CreateFileScreen( ) { WireTextField( textState = createFileViewModel.fileNameTextFieldState, - placeholderText = stringResource(R.string.cells_folder_name), - labelText = stringResource(R.string.cells_folder_name).uppercase(Locale.getDefault()), + placeholderText = stringResource(R.string.cell_file_name), + labelText = stringResource(R.string.cell_file_name).uppercase(Locale.getDefault()), modifier = Modifier .padding(it) .padding( diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt index 5d8b85b4f30..d38def73308 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt @@ -18,5 +18,6 @@ package com.wire.android.feature.cells.ui.create.createfile data class CreateFileScreenNavArgs( - val extension: String, + val uuid: String, + val fileType: FileType, ) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt index b2091901a19..c0aab7694ea 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt @@ -28,6 +28,11 @@ import com.wire.android.feature.cells.ui.common.validateFileName import com.wire.android.feature.cells.ui.navArgs import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.common.textfield.textAsFlow +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 com.wire.kalium.common.functional.onFailure +import com.wire.kalium.common.functional.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map @@ -36,12 +41,15 @@ import javax.inject.Inject @HiltViewModel class CreateFileViewModel @Inject constructor( - val savedStateHandle: SavedStateHandle, + private val savedStateHandle: SavedStateHandle, + private val createPresentationFileUseCase: CreatePresentationFileUseCase, + private val createDocumentFileUseCase: CreateDocumentFileUseCase, + private val createSpreadsheetFileUseCase: CreateSpreadsheetFileUseCase ) : ActionsViewModel() { private val navArgs: CreateFileScreenNavArgs = savedStateHandle.navArgs() - val fileExtension: String = navArgs.extension + val fileExtension: String = navArgs.fileType.getExtension() val fileNameTextFieldState: TextFieldState = TextFieldState() @@ -59,6 +67,39 @@ class CreateFileViewModel @Inject constructor( } } } + + internal fun createFile(fileName: String) = viewModelScope.launch { + viewState = viewState.copy(loading = true) + + val fullPath = "${navArgs.uuid}/${fileName.trim()}" + + val result = when (navArgs.fileType) { + FileType.DOCUMENT -> + createDocumentFileUseCase(fullPath) + + FileType.SPREADSHEET -> + createSpreadsheetFileUseCase(fullPath) + + FileType.PRESENTATION -> + createPresentationFileUseCase(fullPath) + } + + result.onSuccess { + sendAction(CreateFileViewModelAction.Success) + }.onFailure { + sendAction(CreateFileViewModelAction.Failure) + } + + viewState = viewState.copy(loading = false) + } + + + fun FileType.getExtension(): String = + when (this) { + FileType.PRESENTATION -> ".pptx" + FileType.DOCUMENT -> ".docx" + FileType.SPREADSHEET -> ".xlsx" + } } sealed interface CreateFileViewModelAction { diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/FileType.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/FileType.kt new file mode 100644 index 00000000000..81577abb450 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/FileType.kt @@ -0,0 +1,22 @@ +/* + * 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.feature.cells.ui.create.createfile + +enum class FileType { + DOCUMENT, PRESENTATION, SPREADSHEET +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderViewModel.kt index c3fdd7fe185..9ad0df0625b 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderViewModel.kt @@ -28,7 +28,7 @@ import com.wire.android.feature.cells.ui.common.validateFileName import com.wire.android.feature.cells.ui.navArgs import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.common.textfield.textAsFlow -import com.wire.kalium.cells.domain.usecase.CreateFolderUseCase +import com.wire.kalium.cells.domain.usecase.create.CreateFolderUseCase import com.wire.kalium.common.functional.onFailure import com.wire.kalium.common.functional.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel 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 54608dac720..41ed4f1e136 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 @@ -34,7 +34,7 @@ import com.wire.android.util.addBeforeExtension import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.android.util.ui.UIText import com.wire.kalium.cells.domain.model.NodeVersion -import com.wire.kalium.cells.domain.usecase.DownloadCellVersionUseCase +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 com.wire.kalium.common.functional.onFailure 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 ad3278a93bc..85c94eb10fe 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 @@ -31,7 +31,7 @@ import com.wire.android.feature.cells.util.FileHelper import com.wire.android.feature.cells.util.FileNameResolver import com.wire.kalium.cells.domain.model.Node import com.wire.kalium.cells.domain.usecase.DeleteCellAssetUseCase -import com.wire.kalium.cells.domain.usecase.DownloadCellFileUseCase +import com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase import com.wire.kalium.cells.domain.usecase.GetAllTagsUseCase import com.wire.kalium.cells.domain.usecase.GetEditorUrlUseCase import com.wire.kalium.cells.domain.usecase.GetPaginatedFilesFlowUseCase 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 06a1df84bd5..97df51f8b10 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 @@ -31,7 +31,7 @@ import com.wire.android.util.ui.resolveForTest import com.wire.android.util.ui.toUIText import com.wire.kalium.cells.domain.model.NodeVersion import com.wire.kalium.cells.domain.model.PreSignedUrl -import com.wire.kalium.cells.domain.usecase.DownloadCellVersionUseCase +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 com.wire.kalium.common.error.CoreFailure diff --git a/kalium b/kalium index cd57cddc8b0..5937491e1dc 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit cd57cddc8b0a64f5d029d564bf23cf5bc50bf90b +Subproject commit 5937491e1dc7b6dad3f411351913d01b94c64555 From b82d73d92c625a4b09405e2ed46c7270341c7646 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 23 Dec 2025 16:52:35 +0100 Subject: [PATCH 03/10] feat: unit test --- .../cells/ui/ConversationFilesScreen.kt | 2 +- .../ui/create/FileTypeBottomSheetDialog.kt | 2 +- .../{createfile => file}/CreateFileScreen.kt | 2 +- .../CreateFileScreenNavArgs.kt | 2 +- .../CreateFileViewModel.kt | 2 +- .../create/{createfile => file}/FileType.kt | 2 +- .../CreateFolderScreen.kt | 2 +- .../CreateFolderScreenNavArgs.kt | 2 +- .../CreateFolderViewModel.kt | 2 +- .../ui/create/file/CreateFileViewModelTest.kt | 266 ++++++++++++++++++ .../folder/CreateFolderViewModelTest.kt | 167 +++++++++++ kalium | 2 +- 12 files changed, 443 insertions(+), 10 deletions(-) rename features/cells/src/main/java/com/wire/android/feature/cells/ui/create/{createfile => file}/CreateFileScreen.kt (99%) rename features/cells/src/main/java/com/wire/android/feature/cells/ui/create/{createfile => file}/CreateFileScreenNavArgs.kt (92%) rename features/cells/src/main/java/com/wire/android/feature/cells/ui/create/{createfile => file}/CreateFileViewModel.kt (98%) rename features/cells/src/main/java/com/wire/android/feature/cells/ui/create/{createfile => file}/FileType.kt (92%) rename features/cells/src/main/java/com/wire/android/feature/cells/ui/create/{createfolder => folder}/CreateFolderScreen.kt (99%) rename features/cells/src/main/java/com/wire/android/feature/cells/ui/create/{createfolder => folder}/CreateFolderScreenNavArgs.kt (92%) rename features/cells/src/main/java/com/wire/android/feature/cells/ui/create/{createfolder => folder}/CreateFolderViewModel.kt (98%) create mode 100644 features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/file/CreateFileViewModelTest.kt create mode 100644 features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModelTest.kt 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 138898125e9..65773c0e31a 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 @@ -53,7 +53,7 @@ import com.wire.android.feature.cells.R import com.wire.android.feature.cells.domain.model.AttachmentFileType import com.wire.android.feature.cells.ui.common.Breadcrumbs import com.wire.android.feature.cells.ui.create.FileTypeBottomSheetDialog -import com.wire.android.feature.cells.ui.create.createfile.CreateFileScreenNavArgs +import com.wire.android.feature.cells.ui.create.file.CreateFileScreenNavArgs import com.wire.android.feature.cells.ui.destinations.AddRemoveTagsScreenDestination import com.wire.android.feature.cells.ui.destinations.ConversationFilesWithSlideInTransitionScreenDestination import com.wire.android.feature.cells.ui.destinations.CreateFileScreenDestination diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt index 60ba8b40a5b..c5370ebdbd6 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/FileTypeBottomSheetDialog.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.wire.android.feature.cells.R -import com.wire.android.feature.cells.ui.create.createfile.FileType +import com.wire.android.feature.cells.ui.create.file.FileType import com.wire.android.ui.common.bottomsheet.MenuBottomSheetItem import com.wire.android.ui.common.bottomsheet.WireMenuModalSheetContent import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreen.kt similarity index 99% rename from features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt rename to features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreen.kt index 094de341500..9de6f8459ec 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreen.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.android.feature.cells.ui.create.createfile +package com.wire.android.feature.cells.ui.create.file import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreenNavArgs.kt similarity index 92% rename from features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt rename to features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreenNavArgs.kt index d38def73308..28e2669c1fc 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileScreenNavArgs.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileScreenNavArgs.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.android.feature.cells.ui.create.createfile +package com.wire.android.feature.cells.ui.create.file data class CreateFileScreenNavArgs( val uuid: String, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModel.kt similarity index 98% rename from features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt rename to features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModel.kt index c0aab7694ea..235fcf42b4c 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/CreateFileViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/CreateFileViewModel.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.android.feature.cells.ui.create.createfile +package com.wire.android.feature.cells.ui.create.file import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/FileType.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/FileType.kt similarity index 92% rename from features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/FileType.kt rename to features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/FileType.kt index 81577abb450..6063ce1f733 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfile/FileType.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/file/FileType.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.android.feature.cells.ui.create.createfile +package com.wire.android.feature.cells.ui.create.file enum class FileType { DOCUMENT, PRESENTATION, SPREADSHEET diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderScreen.kt similarity index 99% rename from features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreen.kt rename to features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderScreen.kt index 91638762837..dfcfe634e7d 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderScreen.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.android.feature.cells.ui.create.createfolder +package com.wire.android.feature.cells.ui.create.folder import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreenNavArgs.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderScreenNavArgs.kt similarity index 92% rename from features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreenNavArgs.kt rename to features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderScreenNavArgs.kt index d0d385d291f..0512d1b58df 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderScreenNavArgs.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderScreenNavArgs.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.android.feature.cells.ui.create.createfolder +package com.wire.android.feature.cells.ui.create.folder data class CreateFolderScreenNavArgs( val uuid: String? diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModel.kt similarity index 98% rename from features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderViewModel.kt rename to features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModel.kt index 9ad0df0625b..a660699839d 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/createfolder/CreateFolderViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModel.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.android.feature.cells.ui.create.createfolder +package com.wire.android.feature.cells.ui.create.folder import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.getValue 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 new file mode 100644 index 00000000000..a59ea857a06 --- /dev/null +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/file/CreateFileViewModelTest.kt @@ -0,0 +1,266 @@ +/* + * 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.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 +import com.wire.kalium.cells.domain.usecase.create.CreateSpreadsheetFileUseCase +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.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class CreateFileViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + + @BeforeEach + fun setUp() { + Dispatchers.setMain(testDispatcher) + } + + @AfterEach + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `given empty file name, when name changes, then save is disabled and no error`() = runTest(StandardTestDispatcher()) { + // Given + val (_, viewModel) = Arrangement() + .withFileTypeReturning(FileType.DOCUMENT) + .arrange() + + // When + viewModel.fileNameTextFieldState.edit { replace(0, length, "") } + advanceUntilIdle() + + // Then + assertFalse(viewModel.viewState.saveEnabled) + assertEquals(null, viewModel.viewState.error) + } + + + @Test + fun `given valid file name, when name changes, then save is enabled and no error`() = runTest { + // Given + val (_, viewModel) = Arrangement() + .withFileTypeReturning(FileType.DOCUMENT) + .arrange() + + // When + viewModel.fileNameTextFieldState.edit { replace(0, length, "Valid Name") } + advanceUntilIdle() + + // Then + assertTrue(viewModel.viewState.saveEnabled) + assertEquals(null, viewModel.viewState.error) + } + + @Test + fun `given invalid file name, when name changes, then save is disabled and error is shown`() = runTest { + // Given + val (_, viewModel) = Arrangement() + .withFileTypeReturning(FileType.DOCUMENT) + .arrange() + + // When + viewModel.fileNameTextFieldState.edit { replace(0, length, "Invalid/Name") } + advanceUntilIdle() + + // Then + assertFalse(viewModel.viewState.saveEnabled) + assertEquals(FileNameError.InvalidName, viewModel.viewState.error) + } + + + @Test + fun `given document file type, when createFile is called, then document use case is invoked`() = runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .withFileTypeReturning(FileType.DOCUMENT) + .withCreateDocumentFileUseCaseReturning(Either.Right(Unit)) + .arrange() + val fileName = "NewDoc" + + // When + viewModel.createFile(fileName) + advanceUntilIdle() + + // Then + coVerify(exactly = 1) { arrangement.createDocumentFileUseCase(any()) } + assertEquals(CreateFileViewModelAction.Success, viewModel.actions.first()) + } + + @Test + fun `given failure from createDocumentUseCase, when createFile is called, then failure action is sent`() = runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .withFileTypeReturning(FileType.DOCUMENT) + .withCreateDocumentFileUseCaseReturning(Either.Left(CoreFailure.InvalidEventSenderID)) + .arrange() + val fileName = "NewDoc" + + // When + viewModel.createFile(fileName) + advanceUntilIdle() + + // Then + coVerify(exactly = 1) { arrangement.createDocumentFileUseCase(any()) } + assertEquals(CreateFileViewModelAction.Failure, viewModel.actions.first()) + } + + @Test + fun `given spreadsheet file type, when createFile is called, then spreadsheet use case is invoked`() = runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .withFileTypeReturning(FileType.SPREADSHEET) + .withCreateSpreadsheetFileUseCaseReturning(Either.Right(Unit)) + .arrange() + val fileName = "NewSheet" + + // When + viewModel.createFile(fileName) + advanceUntilIdle() + + // Then + coVerify(exactly = 1) { arrangement.createSpreadsheetFileUseCase(any()) } + assertEquals(CreateFileViewModelAction.Success, viewModel.actions.first()) + } + + @Test + fun `given failure from createSpreadSheetUseCase, when createFile is called, then failure action is sent`() = runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .withFileTypeReturning(FileType.SPREADSHEET) + .withCreateSpreadsheetFileUseCaseReturning(Either.Left(CoreFailure.InvalidEventSenderID)) + .arrange() + val fileName = "NewSheet" + + // When + viewModel.createFile(fileName) + advanceUntilIdle() + + // Then + coVerify(exactly = 1) { arrangement.createSpreadsheetFileUseCase(any()) } + assertEquals(CreateFileViewModelAction.Failure, viewModel.actions.first()) + } + + @Test + fun `given presentation file type, when createFile is called, then presentation use case is invoked`() = runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .withFileTypeReturning(FileType.PRESENTATION) + .withCreatePresentationFileUseCaseReturning(Either.Right(Unit)) + .arrange() + val fileName = "NewSlides" + + // When + viewModel.createFile(fileName) + advanceUntilIdle() + + // Then + coVerify(exactly = 1) { arrangement.createPresentationFileUseCase(any()) } + assertEquals(CreateFileViewModelAction.Success, viewModel.actions.first()) + } + + @Test + fun `given failure from createPresentationUseCase, when createFile is called, then failure action is sent`() = runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .withFileTypeReturning(FileType.PRESENTATION) + .withCreatePresentationFileUseCaseReturning(Either.Left(CoreFailure.InvalidEventSenderID)) + .arrange() + val fileName = "NewSlides" + + // When + viewModel.createFile(fileName) + advanceUntilIdle() + + // Then + coVerify(exactly = 1) { arrangement.createPresentationFileUseCase(any()) } + assertEquals(CreateFileViewModelAction.Failure, viewModel.actions.first()) + } + + private class Arrangement { + + @MockK + lateinit var savedStateHandle: SavedStateHandle + + @MockK + lateinit var createPresentationFileUseCase: CreatePresentationFileUseCase + + @MockK + lateinit var createDocumentFileUseCase: CreateDocumentFileUseCase + + @MockK + lateinit var createSpreadsheetFileUseCase: CreateSpreadsheetFileUseCase + + private val testUuid = "test-uuid" + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + every { savedStateHandle.get("uuid") } returns testUuid + } + + private val viewModel by lazy { + CreateFileViewModel( + savedStateHandle = savedStateHandle, + createPresentationFileUseCase = createPresentationFileUseCase, + createDocumentFileUseCase = createDocumentFileUseCase, + createSpreadsheetFileUseCase = createSpreadsheetFileUseCase, + ) + } + + fun withFileTypeReturning(result: FileType) = apply { + every { savedStateHandle.get("fileType") } returns result + } + + fun withCreateDocumentFileUseCaseReturning(result: Either) = apply { + coEvery { createDocumentFileUseCase(any()) } returns result + } + + fun withCreateSpreadsheetFileUseCaseReturning(result: Either) = apply { + coEvery { createSpreadsheetFileUseCase(any()) } returns result + } + + fun withCreatePresentationFileUseCaseReturning(result: Either) = apply { + coEvery { createPresentationFileUseCase(any()) } returns result + } + + fun arrange() = this to viewModel + } +} 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 new file mode 100644 index 00000000000..25509aba7e4 --- /dev/null +++ b/features/cells/src/test/kotlin/com/wire/android/feature/cells/ui/create/folder/CreateFolderViewModelTest.kt @@ -0,0 +1,167 @@ +/* + * 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.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 +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 +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class CreateFolderViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + + @BeforeEach + fun setUp() { + Dispatchers.setMain(testDispatcher) + } + + @AfterEach + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `given empty folder name, when name changes, then save is disabled and no error`() = runTest { + // Given + val (_, viewModel) = Arrangement() + .arrange() + + // When + viewModel.folderNameTextFieldState.edit { replace(0, length, "") } + advanceUntilIdle() + + // Then + assertFalse(viewModel.viewState.saveEnabled) + assertEquals(null, viewModel.viewState.error) + } + + @Test + fun `given valid folder name, when name changes, then save is enabled and no error`() = runTest { + // Given + val (_, viewModel) = Arrangement() + .arrange() + + // When + viewModel.folderNameTextFieldState.edit { replace(0, length, "Valid Folder") } + advanceUntilIdle() + + // Then + assertTrue(viewModel.viewState.saveEnabled) + assertEquals(null, viewModel.viewState.error) + } + + @Test + fun `given invalid folder name, when name changes, then save is disabled and error is shown`() = runTest { + // Given + val (_, viewModel) = Arrangement() + .arrange() + + // When + viewModel.folderNameTextFieldState.edit { replace(0, length, "Invalid/Folder") } + advanceUntilIdle() + + // Then + assertFalse(viewModel.viewState.saveEnabled) + assertEquals(FileNameError.InvalidName, viewModel.viewState.error) + } + + @Test + fun `given valid folder name, when createFolder is called, then create folder use case is invoked`() = runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .withCreateFolderUseCaseReturning(Either.Right(Unit)) + .arrange() + val folderName = "NewFolder" + + // When + viewModel.createFolder(folderName) + advanceUntilIdle() + + // Then + coVerify(exactly = 1) { arrangement.createFolderUseCase(any()) } + assertEquals(CreateFolderViewModelAction.Success, viewModel.actions.first()) + } + + @Test + fun `given failure from createFolderUseCase, when createFolder is called, then failure action is sent`() = runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .withCreateFolderUseCaseReturning(Either.Left(CoreFailure.InvalidEventSenderID)) + .arrange() + val folderName = "NewFolder" + + // When + viewModel.createFolder(folderName) + advanceUntilIdle() + + // Then + coVerify(exactly = 1) { arrangement.createFolderUseCase(any()) } + assertEquals(CreateFolderViewModelAction.Failure, viewModel.actions.first()) + } + + private class Arrangement { + + @MockK + lateinit var savedStateHandle: SavedStateHandle + + @MockK + lateinit var createFolderUseCase: CreateFolderUseCase + + private val testUuid = "test-uuid" + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + every { savedStateHandle.get("uuid") } returns testUuid + } + + private val viewModel by lazy { + CreateFolderViewModel( + savedStateHandle = savedStateHandle, + createFolderUseCase = createFolderUseCase, + ) + } + + fun withCreateFolderUseCaseReturning(result: Either) = apply { + coEvery { createFolderUseCase(any()) } returns result + } + + fun arrange() = this to viewModel + } +} diff --git a/kalium b/kalium index 5937491e1dc..6ab3bec41a0 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 5937491e1dc7b6dad3f411351913d01b94c64555 +Subproject commit 6ab3bec41a060c9deedbfb92c2e7e8fc6d68dafc From ae3fe95a93c836fe36ac67cdff09e5a94f1b254c Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 24 Dec 2025 12:34:13 +0100 Subject: [PATCH 04/10] feat: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 5d90c0afb26..90177a0ae2a 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 5d90c0afb2627269e33ca4c36c0afce27d85aa01 +Subproject commit 90177a0ae2a9a1da8ae8b8d503288b2651b942f5 From 1cdd84273c86802984bb49ae671f8c2e808ec9b5 Mon Sep 17 00:00:00 2001 From: ohassine Date: Mon, 29 Dec 2025 10:49:05 +0100 Subject: [PATCH 05/10] feat: detekt --- .../kotlin/com/wire/android/di/accountScoped/CellsModule.kt | 3 ++- .../feature/cells/ui/create/file/CreateFileViewModel.kt | 1 - .../feature/cells/ui/create/file/CreateFileViewModelTest.kt | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) 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 44d4b95b2e0..1cf99ae7c0f 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 @@ -155,7 +155,8 @@ class CellsModule { @ViewModelScoped @Provides - fun provideCreatePresentationFileUseCase(cellsScope: CellsScope): CreatePresentationFileUseCase = cellsScope.createPresentationFileUseCase + fun provideCreatePresentationFileUseCase(cellsScope: CellsScope): CreatePresentationFileUseCase = + cellsScope.createPresentationFileUseCase @ViewModelScoped @Provides 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 235fcf42b4c..44a6392cef7 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 @@ -93,7 +93,6 @@ class CreateFileViewModel @Inject constructor( viewState = viewState.copy(loading = false) } - fun FileType.getExtension(): String = when (this) { FileType.PRESENTATION -> ".pptx" 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 a59ea857a06..6fde9f7bd20 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 @@ -73,7 +73,6 @@ class CreateFileViewModelTest { assertEquals(null, viewModel.viewState.error) } - @Test fun `given valid file name, when name changes, then save is enabled and no error`() = runTest { // Given @@ -106,7 +105,6 @@ class CreateFileViewModelTest { assertEquals(FileNameError.InvalidName, viewModel.viewState.error) } - @Test fun `given document file type, when createFile is called, then document use case is invoked`() = runTest { // Given From 750437c3088d330447173c4dafc6be58369b288c Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 7 Jan 2026 16:26:22 +0100 Subject: [PATCH 06/10] chore: update kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 2ce2bf32569..5b9b0ec2122 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 2ce2bf325699674d5cf26b7b2b229d15803fabec +Subproject commit 5b9b0ec212214f4a55b11cb76d986c756c94b37f From 07d21dd6d8b685f2abfbbf4fa837d219d244d08f Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 8 Jan 2026 09:42:54 +0100 Subject: [PATCH 07/10] feat: cleanup --- .../cells/ui/create/file/CreateFileScreen.kt | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) 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 9de6f8459ec..5db7dd1b3cb 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 @@ -98,34 +98,28 @@ fun CreateFileScreen( ) }, bottomBar = { - AnimatedVisibility( - visible = true, - enter = fadeIn() + expandVertically(), - exit = shrinkVertically() + fadeOut(), + Surface( + color = MaterialTheme.wireColorScheme.background, + shadowElevation = MaterialTheme.wireDimensions.bottomNavigationShadowElevation ) { - Surface( - color = MaterialTheme.wireColorScheme.background, - shadowElevation = MaterialTheme.wireDimensions.bottomNavigationShadowElevation + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .padding(dimensions().spacing16x) ) { - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier - .padding(dimensions().spacing16x) - ) { - with(createFileViewModel) { - WirePrimaryButton( - text = stringResource(R.string.cells_create_file), - onClick = { - createFileViewModel.createFile(fileNameTextFieldState.text.toString()) - }, - state = if (viewState.saveEnabled && !viewState.loading) { - WireButtonState.Default - } else { - WireButtonState.Disabled - }, - loading = viewState.loading - ) - } + with(createFileViewModel) { + WirePrimaryButton( + text = stringResource(R.string.cells_create_file), + onClick = { + createFileViewModel.createFile(fileNameTextFieldState.text.toString()) + }, + state = if (viewState.saveEnabled && !viewState.loading) { + WireButtonState.Default + } else { + WireButtonState.Disabled + }, + loading = viewState.loading + ) } } } @@ -152,6 +146,7 @@ fun CreateFileScreen( resultNavigator.setResult(true) resultNavigator.navigateBack() } + CreateFileViewModelAction.Failure -> { showErrorDialog.value = true } From d2d3970c9c2343bb4c2b03ae103e31cf1fd490fe Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 8 Jan 2026 09:43:01 +0100 Subject: [PATCH 08/10] feat: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 5b9b0ec2122..85acab7df12 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 5b9b0ec212214f4a55b11cb76d986c756c94b37f +Subproject commit 85acab7df12067149d81316bb51c241d04673e7a From abe7b5d800349bef2990ea159c610fc877d6b5e3 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 8 Jan 2026 09:45:42 +0100 Subject: [PATCH 09/10] feat: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 85acab7df12..5b9b0ec2122 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 85acab7df12067149d81316bb51c241d04673e7a +Subproject commit 5b9b0ec212214f4a55b11cb76d986c756c94b37f From 7d0317de0d31c4df111395f93f7df35232b963ad Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 8 Jan 2026 10:06:01 +0100 Subject: [PATCH 10/10] chore: cleanup --- .../android/feature/cells/ui/create/file/CreateFileScreen.kt | 5 ----- 1 file changed, 5 deletions(-) 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 5db7dd1b3cb..1d720dc6c4d 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 @@ -17,11 +17,6 @@ */ package com.wire.android.feature.cells.ui.create.file -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding