From 5ff36d608c09cf85bf0d43a1923e112b9db4cb43 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 13 May 2026 19:46:44 +0200 Subject: [PATCH 1/4] feat: In-app image viewer --- .../feature/cells/ui/AllFilesScreen.kt | 17 ++ .../feature/cells/ui/CellScreenContent.kt | 2 + .../android/feature/cells/ui/CellViewModel.kt | 10 + .../cells/ui/ConversationFilesScreen.kt | 17 ++ .../ui/imageviewer/CellImageViewerNavArgs.kt | 27 +++ .../ui/imageviewer/CellImageViewerScreen.kt | 191 ++++++++++++++++++ .../imageviewer/CellImageViewerViewModel.kt | 39 ++++ .../cells/ui/recyclebin/RecycleBinScreen.kt | 17 ++ .../feature/cells/ui/search/SearchScreen.kt | 17 ++ .../cells/src/main/res/values/strings.xml | 1 + .../feature/cells/ui/CellViewModelTest.kt | 48 ++++- kalium | 2 +- 12 files changed, 383 insertions(+), 5 deletions(-) create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerNavArgs.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt create mode 100644 features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt 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..fc251a8eb0f 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 @@ -27,9 +27,11 @@ 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.CellImageViewerScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.SearchScreenDestination import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.imageviewer.CellImageViewerNavArgs import com.wire.android.feature.cells.ui.search.DriveSearchScreenType import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.WireNavigator @@ -106,6 +108,21 @@ fun AllFilesScreen( }, isRefreshing = viewModel.isPullToRefresh.collectAsState(), onRefresh = { viewModel.onPullToRefresh() }, + showImageViewer = { file -> + navigator.navigate( + NavigationCommand( + CellImageViewerScreenDestination( + CellImageViewerNavArgs( + localPath = file.localPath, + contentUrl = file.contentUrl, + previewUrl = file.previewUrl, + contentHash = file.contentHash, + fileName = file.name, + ) + ) + ) + ) + }, fileReadyFlow = viewModel.fileReadyFlow, ) } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt index 96ffe31f2b3..229ec25e038 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellScreenContent.kt @@ -98,6 +98,7 @@ internal fun CellScreenContent( lazyListState: LazyListState = rememberLazyListState(), retryEditNodeError: (String) -> Unit = {}, showVersionHistoryScreen: (String, String) -> Unit = { _, _ -> }, + showImageViewer: (CellNodeUi.File) -> Unit = {}, fileReadyFlow: Flow? = emptyFlow(), ) { @@ -245,6 +246,7 @@ internal fun CellScreenContent( is ShowFileDeletedMessage -> showDeleteConfirmation(context, action.isFile, action.permanently) is OpenFolder -> openFolder(action.path, action.title, action.parentFolderUuid) is ShowEditErrorDialog -> editNodeError = action.nodeUuid + is OpenImageViewer -> showImageViewer(action.file) } } 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..5560db21599 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 @@ -32,6 +32,7 @@ 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.domain.model.AttachmentFileType import com.wire.android.feature.cells.ui.model.canOpenWithUrl import com.wire.android.feature.cells.ui.model.localFileAvailable import com.wire.android.feature.cells.ui.model.toUiModel @@ -309,6 +310,10 @@ class CellViewModel @Inject constructor( } private fun openFileContentUrl(file: CellNodeUi.File) { + if (file.assetType == AttachmentFileType.IMAGE) { + sendAction(OpenImageViewer(file)) + return + } file.contentUrl?.let { url -> fileHelper.openAssetUrlWithExternalApp( url = url, @@ -321,6 +326,10 @@ class CellViewModel @Inject constructor( } private fun openLocalFile(file: CellNodeUi.File) { + if (file.assetType == AttachmentFileType.IMAGE) { + sendAction(OpenImageViewer(file)) + return + } file.localPath?.let { path -> fileHelper.openAssetFileWithExternalApp( localPath = path.toPath(), @@ -500,6 +509,7 @@ internal data class ShowFileDeletedMessage(val isFile: Boolean, val permanently: internal data object RefreshData : CellViewAction internal data class OpenFolder(val path: String, val title: String, val parentFolderUuid: String?) : CellViewAction internal data class ShowEditErrorDialog(val nodeUuid: String) : CellViewAction +internal data class OpenImageViewer(val file: CellNodeUi.File) : CellViewAction internal enum class CellError(val message: Int) { NO_APP_FOUND(R.string.no_app_found), 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..e921a6d9582 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 @@ -45,6 +45,7 @@ import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.ramcosta.composedestinations.generated.cells.destinations.AddRemoveTagsScreenDestination +import com.ramcosta.composedestinations.generated.cells.destinations.CellImageViewerScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.ConversationFilesWithSlideInTransitionScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.CreateFileScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.CreateFolderScreenDestination @@ -56,6 +57,7 @@ import com.ramcosta.composedestinations.generated.cells.destinations.SearchScree import com.ramcosta.composedestinations.generated.cells.destinations.VersionHistoryScreenDestination import com.wire.android.feature.cells.R import com.wire.android.feature.cells.domain.model.AttachmentFileType +import com.wire.android.feature.cells.ui.imageviewer.CellImageViewerNavArgs import com.wire.android.feature.cells.ui.create.FileTypeBottomSheetDialog import com.wire.android.feature.cells.ui.create.file.CreateFileScreenNavArgs import com.wire.android.feature.cells.ui.dialog.CellsNewActionBottomSheet @@ -350,6 +352,21 @@ internal fun ConversationFilesScreenContent( showVersionHistoryScreen = { uuid, fileName -> navigator.navigate(NavigationCommand(VersionHistoryScreenDestination(uuid, fileName))) }, + showImageViewer = { file -> + navigator.navigate( + NavigationCommand( + CellImageViewerScreenDestination( + CellImageViewerNavArgs( + localPath = file.localPath, + contentUrl = file.contentUrl, + previewUrl = file.previewUrl, + contentHash = file.contentHash, + fileName = file.name, + ) + ) + ) + ) + }, retryEditNodeError = { retryEditNodeError(it) }, isRefreshing = isRefreshing, onRefresh = onRefresh, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerNavArgs.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerNavArgs.kt new file mode 100644 index 00000000000..fa552522d7b --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerNavArgs.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.cells.ui.imageviewer + +data class CellImageViewerNavArgs( + val localPath: String? = null, + val contentUrl: String? = null, + val previewUrl: String? = null, + val contentHash: String? = null, + val fileName: String? = null, +) + diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt new file mode 100644 index 00000000000..0e4f2885782 --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt @@ -0,0 +1,191 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received 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.imageviewer + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import coil3.compose.rememberAsyncImagePainter +import coil3.request.CachePolicy +import coil3.request.ImageRequest +import coil3.request.crossfade +import com.wire.android.feature.cells.R +import com.wire.android.navigation.WireNavigator +import com.wire.android.navigation.annotation.features.cells.WireCellsDestination +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.topappbar.NavigationIconType +import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar +import com.wire.android.ui.theme.WireTheme + +@WireCellsDestination( + style = PopUpNavigationAnimation::class, + navArgs = CellImageViewerNavArgs::class, +) +@Composable +fun CellImageViewerScreen( + navigator: WireNavigator, + modifier: Modifier = Modifier, + viewModel: CellImageViewerViewModel = hiltViewModel(), +) { + CellImageViewerScreenContent( + localPath = viewModel.localPath, + contentUrl = viewModel.contentUrl, + previewUrl = viewModel.previewUrl, + contentHash = viewModel.contentHash, + fileName = viewModel.fileName, + onNavigateBack = navigator::navigateBack, + modifier = modifier, + ) +} + +@Composable +internal fun CellImageViewerScreenContent( + localPath: String?, + contentUrl: String?, + previewUrl: String?, + contentHash: String?, + fileName: String?, + onNavigateBack: () -> Unit, + modifier: Modifier = Modifier, +) { + WireScaffold( + modifier = modifier, + topBar = { + WireCenterAlignedTopAppBar( + title = fileName ?: stringResource(R.string.conversation_files_title), + navigationIconType = NavigationIconType.Back(), + onNavigationPressed = onNavigateBack, + ) + }, + ) { innerPadding -> + Box( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .background(Color.Black), + contentAlignment = Alignment.Center, + ) { + CellZoomableImage( + localPath = localPath, + contentUrl = contentUrl, + previewUrl = previewUrl, + contentHash = contentHash, + contentDescription = fileName ?: stringResource(R.string.content_description_image_message), + modifier = Modifier.fillMaxSize(), + ) + } + } +} + +@Composable +private fun CellZoomableImage( + localPath: String?, + contentUrl: String?, + previewUrl: String?, + contentHash: String?, + contentDescription: String, + modifier: Modifier = Modifier, +) { + var offsetX by remember { mutableStateOf(0f) } + var offsetY by remember { mutableStateOf(0f) } + var zoom by remember { mutableStateOf(1f) } + val minScale = 1.0f + val maxScale = 3f + + val painter = when { + localPath != null -> rememberAsyncImagePainter(localPath) + contentUrl != null -> rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(contentUrl) + .diskCacheKey(contentHash) + .memoryCacheKey(contentHash) + .diskCachePolicy(CachePolicy.ENABLED) + .build(), + placeholder = previewUrl?.let { + rememberAsyncImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(it) + .diskCacheKey(contentHash) + .memoryCacheKey(contentHash) + .crossfade(true) + .build() + ) + }, + ) + else -> return + } + + Image( + painter = painter, + contentDescription = contentDescription, + modifier = modifier + .graphicsLayer( + scaleX = zoom, + scaleY = zoom, + translationX = offsetX, + translationY = offsetY, + ) + .pointerInput(Unit) { + detectTransformGestures { _, pan, gestureZoom, _ -> + zoom = (zoom * gestureZoom).coerceIn(minScale, maxScale) + if (zoom > 1) { + offsetX += pan.x * zoom + offsetY += pan.y * zoom + } else { + offsetX = 0f + offsetY = 0f + } + } + }, + contentScale = ContentScale.Fit, + ) +} + +@MultipleThemePreviews +@Composable +fun PreviewCellImageViewerScreen() { + WireTheme { + CellImageViewerScreenContent( + localPath = null, + contentUrl = null, + previewUrl = null, + contentHash = null, + fileName = "photo.jpg", + onNavigateBack = {}, + ) + } +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt new file mode 100644 index 00000000000..26cd17776ce --- /dev/null +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.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.feature.cells.ui.imageviewer + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.ramcosta.composedestinations.generated.cells.destinations.CellImageViewerScreenDestination +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class CellImageViewerViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val navArgs: CellImageViewerNavArgs = CellImageViewerScreenDestination.argsFrom(savedStateHandle) + + val localPath: String? = navArgs.localPath + val contentUrl: String? = navArgs.contentUrl + val previewUrl: String? = navArgs.previewUrl + val contentHash: String? = navArgs.contentHash + val fileName: String? = navArgs.fileName +} + 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..bff78919dec 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 @@ -34,9 +34,11 @@ import com.wire.android.feature.cells.ui.CellFilesNavArgs import com.wire.android.feature.cells.ui.CellScreenContent import com.wire.android.feature.cells.ui.CellViewModel import com.wire.android.feature.cells.ui.common.Breadcrumbs +import com.ramcosta.composedestinations.generated.cells.destinations.CellImageViewerScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.ConversationFilesWithSlideInTransitionScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.MoveToFolderScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkScreenDestination +import com.wire.android.feature.cells.ui.imageviewer.CellImageViewerNavArgs import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.WireNavigator @@ -139,6 +141,21 @@ fun RecycleBinScreen( }, showRenameScreen = { }, showAddRemoveTagsScreen = {}, + showImageViewer = { file -> + navigator.navigate( + NavigationCommand( + CellImageViewerScreenDestination( + CellImageViewerNavArgs( + localPath = file.localPath, + contentUrl = file.contentUrl, + previewUrl = file.previewUrl, + contentHash = file.contentHash, + fileName = file.name, + ) + ) + ) + ) + }, isRefreshing = cellViewModel.isPullToRefresh.collectAsState(), onRefresh = { cellViewModel.onPullToRefresh() } ) 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..bf860e85994 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 @@ -40,6 +40,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.collectAsLazyPagingItems import com.ramcosta.composedestinations.generated.cells.destinations.AddRemoveTagsScreenDestination +import com.ramcosta.composedestinations.generated.cells.destinations.CellImageViewerScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.MoveToFolderScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.PublicLinkScreenDestination import com.ramcosta.composedestinations.generated.cells.destinations.RenameNodeScreenDestination @@ -47,6 +48,7 @@ import com.ramcosta.composedestinations.generated.cells.destinations.VersionHist import com.wire.android.feature.cells.R import com.wire.android.feature.cells.ui.CellScreenContent import com.wire.android.feature.cells.ui.CellViewModel +import com.wire.android.feature.cells.ui.imageviewer.CellImageViewerNavArgs import com.wire.android.feature.cells.ui.model.CellNodeUi import com.wire.android.feature.cells.ui.search.filter.FilterChipsRow import com.wire.android.feature.cells.ui.search.filter.bottomsheet.FilterByTypeBottomSheet @@ -233,6 +235,21 @@ fun SearchScreen( showVersionHistoryScreen = { uuid, fileName -> navigator.navigate(NavigationCommand(VersionHistoryScreenDestination(uuid, fileName))) }, + showImageViewer = { file -> + navigator.navigate( + NavigationCommand( + CellImageViewerScreenDestination( + CellImageViewerNavArgs( + localPath = file.localPath, + contentUrl = file.contentUrl, + previewUrl = file.previewUrl, + contentHash = file.contentHash, + fileName = file.name, + ) + ) + ) + ) + }, retryEditNodeError = { cellViewModel.editNode(it) }, isRefreshing = remember { mutableStateOf(false) }, onRefresh = { }, diff --git a/features/cells/src/main/res/values/strings.xml b/features/cells/src/main/res/values/strings.xml index 29c5cce3a2f..adb7b1e4d7c 100644 --- a/features/cells/src/main/res/values/strings.xml +++ b/features/cells/src/main/res/values/strings.xml @@ -250,4 +250,5 @@ Z to A Smallest first Largest first + Image 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..4f6ac9a25ed 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 @@ -25,6 +25,7 @@ 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.domain.model.AttachmentFileType import com.wire.android.feature.cells.ui.edit.OnlineEditor import com.wire.android.feature.cells.ui.model.OpenLoadState import com.wire.android.feature.cells.ui.model.toUiModel @@ -118,23 +119,61 @@ class CellViewModelTest { } @Test - fun `given view model when file clicked and local file is present file is opened`() = runTest { + fun `given view model when image file clicked and local file is present then in-app viewer is opened`() = runTest { val (arrangement, viewModel) = Arrangement() .withLoadSuccess() .arrange() - viewModel.sendIntent(CellViewIntent.OnItemClick(testFiles[0].toUiModel())) + viewModel.actions.test { + viewModel.sendIntent(CellViewIntent.OnItemClick(testFiles[0].toUiModel())) + + val action = awaitItem() + assert(action is OpenImageViewer) + coVerify(exactly = 0) { arrangement.fileHelper.openAssetFileWithExternalApp(any(), any(), any(), any()) } + } + } + + @Test + fun `given view model when non-image file clicked and local file is present then external app is opened`() = runTest { + val (arrangement, viewModel) = Arrangement() + .withLoadSuccess() + .arrange() + + val nonImageFile = testFiles[0].copy(mimeType = "application/pdf").toUiModel() + + viewModel.sendIntent(CellViewIntent.OnItemClick(nonImageFile)) coVerify(exactly = 1) { arrangement.fileHelper.openAssetFileWithExternalApp(any(), any(), any(), any()) } } @Test - fun `given view model when file clicked and local file is not present and url is openable then url is opened`() = runTest { + fun `given view model when image file clicked and local file is not present and url is openable then in-app viewer is opened`() = runTest { + val (arrangement, viewModel) = Arrangement() + .withLoadSuccess() + .arrange() + + val testFile = testFiles[0].copy( + localPath = null, + contentUrl = "https://example.com/file" + ) + + viewModel.actions.test { + viewModel.sendIntent(CellViewIntent.OnItemClick(testFile.toUiModel())) + + val action = awaitItem() + assert(action is OpenImageViewer) + coVerify(exactly = 0) { arrangement.fileHelper.openAssetUrlWithExternalApp(any(), any(), any()) } + } + } + + @Test + fun `given view model when non-image file clicked and local file is not present and url is openable then url is opened`() = runTest { val (arrangement, viewModel) = Arrangement() .withLoadSuccess() .arrange() val testFile = testFiles[0].copy( + mimeType = "application/pdf", localPath = null, contentUrl = "https://example.com/file" ) @@ -171,7 +210,8 @@ class CellViewModelTest { .arrange() // File has localPath from DB but also carries an error state (stale UI state) - val testFile = testFiles[0].copy(localPath = "localPath", contentUrl = null).toUiModel() + // Use a non-image file so we can verify the external app opener is called + val testFile = testFiles[0].copy(localPath = "localPath", contentUrl = null, mimeType = "application/pdf").toUiModel() .copy(openLoadState = OpenLoadState.Error) viewModel.sendIntent(CellViewIntent.OnItemClick(testFile)) diff --git a/kalium b/kalium index f69a608d512..541dc48c2ee 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit f69a608d5126385b2a1284845657c0bbf11f9c65 +Subproject commit 541dc48c2eefc799199b835388293367d4631ee2 From 37a16c435a02aab4484646f0fb7b1e72e61eeb57 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 10 Jun 2026 14:13:24 +0100 Subject: [PATCH 2/4] chore: remove hilt --- .../com/wire/android/feature/cells/ui/CellViewModel.kt | 1 - .../wire/android/feature/cells/ui/CellsViewModelFactory.kt | 5 +++++ .../wire/android/feature/cells/ui/CellsViewModelGraph.kt | 4 ++++ .../feature/cells/ui/imageviewer/CellImageViewerScreen.kt | 4 ++-- .../cells/ui/imageviewer/CellImageViewerViewModel.kt | 7 +------ 5 files changed, 12 insertions(+), 9 deletions(-) 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 f1e0a65c0f3..6516538dfdd 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 @@ -33,7 +33,6 @@ 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.domain.model.AttachmentFileType import com.wire.android.feature.cells.ui.model.canOpenWithUrl import com.wire.android.feature.cells.ui.model.localFileAvailable import com.wire.android.feature.cells.ui.model.toUiModel diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellsViewModelFactory.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellsViewModelFactory.kt index 301a6f35584..dcc141d31d9 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellsViewModelFactory.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellsViewModelFactory.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.SavedStateHandle import com.wire.android.feature.cells.ui.create.file.CreateFileViewModel import com.wire.android.feature.cells.ui.create.folder.CreateFolderViewModel import com.wire.android.feature.cells.ui.edit.OnlineEditor +import com.wire.android.feature.cells.ui.imageviewer.CellImageViewerViewModel import com.wire.android.feature.cells.ui.movetofolder.MoveToFolderViewModel import com.wire.android.feature.cells.ui.publiclink.PublicLinkViewModel import com.wire.android.feature.cells.ui.publiclink.settings.expiration.PublicLinkExpirationScreenViewModel @@ -209,4 +210,8 @@ class CellsViewModelFactory @Inject constructor( getEditorUrl = getEditorUrl, dispatchers = dispatchers, ) + + internal fun cellImageViewerViewModel(savedStateHandle: SavedStateHandle) = CellImageViewerViewModel( + savedStateHandle = savedStateHandle, + ) } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellsViewModelGraph.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellsViewModelGraph.kt index c32fef08d60..9b49fe8ab63 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellsViewModelGraph.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/CellsViewModelGraph.kt @@ -26,6 +26,7 @@ import com.wire.android.di.metro.MetroViewModelGraph import com.wire.android.di.metro.metroSavedStateViewModel import com.wire.android.feature.cells.ui.create.file.CreateFileViewModel import com.wire.android.feature.cells.ui.create.folder.CreateFolderViewModel +import com.wire.android.feature.cells.ui.imageviewer.CellImageViewerViewModel import com.wire.android.feature.cells.ui.movetofolder.MoveToFolderViewModel import com.wire.android.feature.cells.ui.publiclink.PublicLinkViewModel import com.wire.android.feature.cells.ui.publiclink.settings.expiration.PublicLinkExpirationScreenViewModel @@ -92,3 +93,6 @@ fun addRemoveTagsViewModel(): AddRemoveTagsViewModel = cellsViewModel { addRemov @Composable fun versionHistoryViewModel(): VersionHistoryViewModel = cellsViewModel { versionHistoryViewModel(it) } + +@Composable +fun cellImageViewerViewModel(): CellImageViewerViewModel = cellsViewModel { cellImageViewerViewModel(it) } diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt index 0e4f2885782..0af15b968f0 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt @@ -36,12 +36,12 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel import coil3.compose.rememberAsyncImagePainter import coil3.request.CachePolicy import coil3.request.ImageRequest import coil3.request.crossfade import com.wire.android.feature.cells.R +import com.wire.android.feature.cells.ui.cellImageViewerViewModel import com.wire.android.navigation.WireNavigator import com.wire.android.navigation.annotation.features.cells.WireCellsDestination import com.wire.android.navigation.style.PopUpNavigationAnimation @@ -59,7 +59,7 @@ import com.wire.android.ui.theme.WireTheme fun CellImageViewerScreen( navigator: WireNavigator, modifier: Modifier = Modifier, - viewModel: CellImageViewerViewModel = hiltViewModel(), + viewModel: CellImageViewerViewModel = cellImageViewerViewModel(), ) { CellImageViewerScreenContent( localPath = viewModel.localPath, diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt index 26cd17776ce..bfd93614b98 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt @@ -20,13 +20,8 @@ package com.wire.android.feature.cells.ui.imageviewer import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import com.ramcosta.composedestinations.generated.cells.destinations.CellImageViewerScreenDestination -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -@HiltViewModel -class CellImageViewerViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, -) : ViewModel() { +class CellImageViewerViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private val navArgs: CellImageViewerNavArgs = CellImageViewerScreenDestination.argsFrom(savedStateHandle) From 06c45e422c901b08d64502e1c17556be04ab608f Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 10 Jun 2026 14:54:09 +0100 Subject: [PATCH 3/4] chore: zoomable container --- .../android/ui/home/gallery/ZoomableImage.kt | 89 +++++++------------ .../ui/common/image/ZoomableImageContainer.kt | 85 ++++++++++++++++++ .../ui/imageviewer/CellImageViewerScreen.kt | 81 ++++++----------- 3 files changed, 143 insertions(+), 112 deletions(-) create mode 100644 core/ui-common/src/main/kotlin/com/wire/android/ui/common/image/ZoomableImageContainer.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/home/gallery/ZoomableImage.kt b/app/src/main/kotlin/com/wire/android/ui/home/gallery/ZoomableImage.kt index 9283b44d628..f3fef3bde6a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/gallery/ZoomableImage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/gallery/ZoomableImage.kt @@ -18,80 +18,53 @@ package com.wire.android.ui.home.gallery -import androidx.compose.foundation.Image -import androidx.compose.foundation.gestures.detectTransformGestures -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import coil3.compose.rememberAsyncImagePainter import coil3.request.CachePolicy import coil3.request.ImageRequest import coil3.request.crossfade +import com.wire.android.ui.common.image.ZoomableImageContainer @Composable -fun ZoomableImage(image: MediaGalleryImage, contentDescription: String, modifier: Modifier = Modifier) { - var offsetX by remember { mutableStateOf(0f) } - var offsetY by remember { mutableStateOf(0f) } - var zoom by remember { mutableStateOf(1f) } - val minScale = 1.0f - val maxScale = 3f +fun ZoomableImage( + image: MediaGalleryImage, + contentDescription: String, + modifier: Modifier = Modifier +) { + val context = LocalContext.current val painter = when (image) { is MediaGalleryImage.PrivateAsset -> image.asset.paint() - is MediaGalleryImage.LocalAsset -> rememberAsyncImagePainter(image.path) - is MediaGalleryImage.UrlAsset -> rememberAsyncImagePainter( - ImageRequest.Builder(LocalContext.current) - .data(image.url) - .diskCacheKey(image.contentHash) - .memoryCacheKey(image.contentHash) - .diskCachePolicy(CachePolicy.ENABLED) - .build(), - placeholder = image.placeholder?.let { - rememberAsyncImagePainter( - ImageRequest.Builder(LocalContext.current) - .diskCacheKey(image.contentHash) - .memoryCacheKey(image.contentHash) - .data(it) - .crossfade(true) - .build() - ) - } - ) + + is MediaGalleryImage.LocalAsset -> + rememberAsyncImagePainter(image.path) + + is MediaGalleryImage.UrlAsset -> + rememberAsyncImagePainter( + ImageRequest.Builder(context) + .data(image.url) + .diskCacheKey(image.contentHash) + .memoryCacheKey(image.contentHash) + .diskCachePolicy(CachePolicy.ENABLED) + .build(), + placeholder = image.placeholder?.let { + rememberAsyncImagePainter( + ImageRequest.Builder(context) + .data(it) + .diskCacheKey(image.contentHash) + .memoryCacheKey(image.contentHash) + .crossfade(true) + .build() + ) + } + ) } - Image( + ZoomableImageContainer( painter = painter, contentDescription = contentDescription, modifier = modifier - .graphicsLayer( - scaleX = zoom, - scaleY = zoom, - translationX = offsetX, - translationY = offsetY, - ) - .pointerInput(Unit) { - detectTransformGestures( - onGesture = { _, pan, gestureZoom, _ -> - zoom = (zoom * gestureZoom).coerceIn(minScale, maxScale) - if (zoom > 1) { - offsetX += pan.x * zoom - offsetY += pan.y * zoom - } else { - offsetX = 0f - offsetY = 0f - } - } - ) - } - .fillMaxSize(), - contentScale = ContentScale.Fit ) } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/image/ZoomableImageContainer.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/image/ZoomableImageContainer.kt new file mode 100644 index 00000000000..80ad336c842 --- /dev/null +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/image/ZoomableImageContainer.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.android.ui.common.image + +import androidx.compose.foundation.Image +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import com.wire.android.ui.common.R +import com.wire.android.ui.common.preview.MultipleThemePreviews + +@Composable +fun ZoomableImageContainer( + painter: Painter, + contentDescription: String, + modifier: Modifier = Modifier, +) { + var offsetX by remember { mutableStateOf(0f) } + var offsetY by remember { mutableStateOf(0f) } + var zoom by remember { mutableStateOf(1f) } + + val minScale = 1f + val maxScale = 3f + + Image( + painter = painter, + contentDescription = contentDescription, + contentScale = ContentScale.Fit, + modifier = modifier + .fillMaxSize() + .graphicsLayer( + scaleX = zoom, + scaleY = zoom, + translationX = offsetX, + translationY = offsetY, + ) + .pointerInput(Unit) { + detectTransformGestures { _, pan, gestureZoom, _ -> + zoom = (zoom * gestureZoom).coerceIn(minScale, maxScale) + + if (zoom > 1f) { + offsetX += pan.x * zoom + offsetY += pan.y * zoom + } else { + offsetX = 0f + offsetY = 0f + } + } + } + ) +} + +@MultipleThemePreviews +@Composable +fun ZoomableImageContainerPreview() { + ZoomableImageContainer( + painter = painterResource(id = R.drawable.mock_image), + contentDescription = "Placeholder image" + ) +} diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt index 0af15b968f0..3198aa2afa1 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerScreen.kt @@ -17,23 +17,14 @@ */ package com.wire.android.feature.cells.ui.imageviewer -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import coil3.compose.rememberAsyncImagePainter @@ -45,6 +36,7 @@ import com.wire.android.feature.cells.ui.cellImageViewerViewModel import com.wire.android.navigation.WireNavigator import com.wire.android.navigation.annotation.features.cells.WireCellsDestination import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.image.ZoomableImageContainer import com.wire.android.ui.common.preview.MultipleThemePreviews import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.NavigationIconType @@ -112,7 +104,7 @@ internal fun CellImageViewerScreenContent( } @Composable -private fun CellZoomableImage( +fun CellZoomableImage( localPath: String?, contentUrl: String?, previewUrl: String?, @@ -120,58 +112,39 @@ private fun CellZoomableImage( contentDescription: String, modifier: Modifier = Modifier, ) { - var offsetX by remember { mutableStateOf(0f) } - var offsetY by remember { mutableStateOf(0f) } - var zoom by remember { mutableStateOf(1f) } - val minScale = 1.0f - val maxScale = 3f + val context = LocalContext.current val painter = when { - localPath != null -> rememberAsyncImagePainter(localPath) - contentUrl != null -> rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalContext.current) - .data(contentUrl) - .diskCacheKey(contentHash) - .memoryCacheKey(contentHash) - .diskCachePolicy(CachePolicy.ENABLED) - .build(), - placeholder = previewUrl?.let { - rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalContext.current) - .data(it) - .diskCacheKey(contentHash) - .memoryCacheKey(contentHash) - .crossfade(true) - .build() - ) - }, - ) + localPath != null -> + rememberAsyncImagePainter(localPath) + + contentUrl != null -> + rememberAsyncImagePainter( + ImageRequest.Builder(context) + .data(contentUrl) + .diskCacheKey(contentHash) + .memoryCacheKey(contentHash) + .diskCachePolicy(CachePolicy.ENABLED) + .build(), + placeholder = previewUrl?.let { + rememberAsyncImagePainter( + ImageRequest.Builder(context) + .data(it) + .diskCacheKey(contentHash) + .memoryCacheKey(contentHash) + .crossfade(true) + .build() + ) + } + ) + else -> return } - Image( + ZoomableImageContainer( painter = painter, contentDescription = contentDescription, modifier = modifier - .graphicsLayer( - scaleX = zoom, - scaleY = zoom, - translationX = offsetX, - translationY = offsetY, - ) - .pointerInput(Unit) { - detectTransformGestures { _, pan, gestureZoom, _ -> - zoom = (zoom * gestureZoom).coerceIn(minScale, maxScale) - if (zoom > 1) { - offsetX += pan.x * zoom - offsetY += pan.y * zoom - } else { - offsetX = 0f - offsetY = 0f - } - } - }, - contentScale = ContentScale.Fit, ) } From 136adf2c454484a0cfc79197e2d85a993cfdddb2 Mon Sep 17 00:00:00 2001 From: ohassine Date: Thu, 11 Jun 2026 08:30:36 +0100 Subject: [PATCH 4/4] chore: detekt --- .../feature/cells/ui/imageviewer/CellImageViewerNavArgs.kt | 1 - .../cells/ui/imageviewer/CellImageViewerViewModel.kt | 1 - .../com/wire/android/feature/cells/ui/CellViewModelTest.kt | 7 +++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerNavArgs.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerNavArgs.kt index fa552522d7b..243dd8a29a6 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerNavArgs.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerNavArgs.kt @@ -24,4 +24,3 @@ data class CellImageViewerNavArgs( val contentHash: String? = null, val fileName: String? = null, ) - diff --git a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt index bfd93614b98..55e78f07c5f 100644 --- a/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt +++ b/features/cells/src/main/java/com/wire/android/feature/cells/ui/imageviewer/CellImageViewerViewModel.kt @@ -31,4 +31,3 @@ class CellImageViewerViewModel(savedStateHandle: SavedStateHandle) : ViewModel() val contentHash: String? = navArgs.contentHash val fileName: String? = navArgs.fileName } - 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 f8ded5868ee..ff0aae6d95d 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 @@ -25,7 +25,6 @@ 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.domain.model.AttachmentFileType import com.wire.android.feature.cells.ui.edit.OnlineEditor import com.wire.android.feature.cells.ui.model.OpenLoadState import com.wire.android.feature.cells.ui.model.toUiModel @@ -33,11 +32,11 @@ 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.GetConversationNameUseCase 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.GetConversationNameUseCase import com.wire.kalium.cells.domain.usecase.GetUserNameUseCase +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 com.wire.kalium.cells.domain.usecase.download.DownloadCellFileUseCase @@ -47,7 +46,6 @@ import com.wire.kalium.cells.domain.usecase.offline.ObserveOfflineFilesUseCase import com.wire.kalium.common.functional.right import com.wire.kalium.network.NetworkState import com.wire.kalium.network.NetworkStateObserver -import kotlinx.coroutines.flow.MutableStateFlow import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -57,6 +55,7 @@ import io.mockk.mockk import io.mockk.mockkObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.UnconfinedTestDispatcher