diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index bd07d88816..79b4a1923c 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -257,6 +257,13 @@ fun NavGraphBuilder.vaultUnlockedGraph( ) attachmentDestination( onNavigateBack = { navController.popBackStack() }, + onNavigateToPreviewAttachment = { cipherId, attachmentId, fileName -> + navController.navigateToPreviewAttachment( + cipherId = cipherId, + attachmentId = attachmentId, + fileName = fileName, + ) + }, ) setupUnlockDestination( onNavigateBack = { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsContent.kt index fc6f4dc8e4..b81eb40d75 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsContent.kt @@ -70,6 +70,7 @@ fun AttachmentsContent( AttachmentListEntry( attachmentItem = it, onDeleteClick = attachmentsHandlers.onDeleteClick, + onItemClick = attachmentsHandlers.onItemClick, cardStyle = viewState.attachments.toListItemCardStyle(index = index), modifier = Modifier .fillMaxWidth() @@ -141,6 +142,7 @@ fun AttachmentsContent( private fun AttachmentListEntry( attachmentItem: AttachmentsState.AttachmentItem, onDeleteClick: (attachmentId: String) -> Unit, + onItemClick: (attachment: AttachmentsState.AttachmentItem) -> Unit, cardStyle: CardStyle, modifier: Modifier = Modifier, ) { @@ -163,7 +165,11 @@ private fun AttachmentListEntry( Row( modifier = modifier .defaultMinSize(minHeight = 60.dp) - .cardStyle(cardStyle = cardStyle, paddingStart = 16.dp) + .cardStyle( + cardStyle = cardStyle, + paddingStart = 16.dp, + onClick = { onItemClick(attachmentItem) }, + ) .testTag("AttachmentRow"), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsNavigation.kt index 0b9da26d91..cecd920f81 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsNavigation.kt @@ -34,10 +34,16 @@ fun SavedStateHandle.toAttachmentsArgs(): AttachmentsArgs { */ fun NavGraphBuilder.attachmentDestination( onNavigateBack: () -> Unit, + onNavigateToPreviewAttachment: ( + cipherId: String, + attachmentId: String, + fileName: String, + ) -> Unit, ) { composableWithSlideTransitions { AttachmentsScreen( onNavigateBack = onNavigateBack, + onNavigateToPreview = onNavigateToPreviewAttachment, ) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreen.kt index a5097c4356..37098ffefa 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreen.kt @@ -41,6 +41,7 @@ fun AttachmentsScreen( viewModel: AttachmentsViewModel = hiltViewModel(), intentManager: IntentManager = LocalIntentManager.current, onNavigateBack: () -> Unit, + onNavigateToPreview: (cipherId: String, attachmentId: String, fileName: String) -> Unit, ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() val attachmentsHandlers = remember(viewModel) { AttachmentsHandlers.create(viewModel) } @@ -61,6 +62,9 @@ fun AttachmentsScreen( } is AttachmentsEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data) + is AttachmentsEvent.NavigateToPreview -> { + onNavigateToPreview(event.cipherId, event.attachmentId, event.fileName) + } } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModel.kt index f3dbd8081e..dfc731bf30 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModel.kt @@ -84,6 +84,7 @@ class AttachmentsViewModel @Inject constructor( AttachmentsAction.ChooseFileClick -> handleChooseFileClick() is AttachmentsAction.FileChoose -> handleFileChoose(action) is AttachmentsAction.DeleteClick -> handleDeleteClick(action) + is AttachmentsAction.ItemClick -> handleItemClick(action) is AttachmentsAction.Internal -> handleInternalAction(action) } } @@ -192,6 +193,16 @@ class AttachmentsViewModel @Inject constructor( } } + private fun handleItemClick(action: AttachmentsAction.ItemClick) { + sendEvent( + AttachmentsEvent.NavigateToPreview( + cipherId = state.cipherId, + attachmentId = action.attachment.id, + fileName = action.attachment.title, + ), + ) + } + private fun handleInternalAction(action: AttachmentsAction.Internal) { when (action) { is AttachmentsAction.Internal.CipherReceive -> handleCipherReceive(action) @@ -436,6 +447,15 @@ sealed class AttachmentsEvent { */ data object NavigateBack : AttachmentsEvent() + /** + * Navigates to preview the attachment. + */ + data class NavigateToPreview( + val cipherId: String, + val attachmentId: String, + val fileName: String, + ) : AttachmentsEvent() + /** * Show chooser sheet. */ @@ -495,12 +515,19 @@ sealed class AttachmentsAction { ) : AttachmentsAction() /** - * User clicked delete an attachment. + * User clicked delete on an attachment. */ data class DeleteClick( val attachmentId: String, ) : AttachmentsAction() + /** + * User clicked on an attachment. + */ + data class ItemClick( + val attachment: AttachmentsState.AttachmentItem, + ) : AttachmentsAction() + /** * Internal ViewModel actions. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/handlers/AttachmentsHandlers.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/handlers/AttachmentsHandlers.kt index cf0f64c8b6..874839bac5 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/handlers/AttachmentsHandlers.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/handlers/AttachmentsHandlers.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.attachments.handlers import com.bitwarden.ui.platform.model.FileData import com.x8bit.bitwarden.ui.vault.feature.attachments.AttachmentsAction +import com.x8bit.bitwarden.ui.vault.feature.attachments.AttachmentsState import com.x8bit.bitwarden.ui.vault.feature.attachments.AttachmentsViewModel /** @@ -13,6 +14,7 @@ data class AttachmentsHandlers( val onChooseFileClick: () -> Unit, val onFileChoose: (FileData) -> Unit, val onDeleteClick: (attachmentId: String) -> Unit, + val onItemClick: (attachment: AttachmentsState.AttachmentItem) -> Unit, val onDismissRequest: () -> Unit, ) { @Suppress("UndocumentedPublicClass") @@ -25,13 +27,10 @@ data class AttachmentsHandlers( AttachmentsHandlers( onBackClick = { viewModel.trySendAction(AttachmentsAction.BackClick) }, onSaveClick = { viewModel.trySendAction(AttachmentsAction.SaveClick) }, - onChooseFileClick = { - viewModel.trySendAction(AttachmentsAction.ChooseFileClick) - }, + onChooseFileClick = { viewModel.trySendAction(AttachmentsAction.ChooseFileClick) }, onFileChoose = { viewModel.trySendAction(AttachmentsAction.FileChoose(it)) }, - onDeleteClick = { - viewModel.trySendAction(AttachmentsAction.DeleteClick(it)) - }, + onDeleteClick = { viewModel.trySendAction(AttachmentsAction.DeleteClick(it)) }, + onItemClick = { viewModel.trySendAction(AttachmentsAction.ItemClick(it)) }, onDismissRequest = { viewModel.trySendAction(AttachmentsAction.DismissDialogClick) }, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/preview/PreviewAttachmentScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/preview/PreviewAttachmentScreen.kt index 4f217d9fab..a8ef333af0 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/preview/PreviewAttachmentScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/preview/PreviewAttachmentScreen.kt @@ -87,7 +87,7 @@ fun PreviewAttachmentScreen( onClick = { viewModel.trySendAction(PreviewAttachmentAction.DownloadClick) }, - modifier = Modifier.testTag("DownloadButton"), + modifier = Modifier.testTag("ToolbarDownloadButton"), ) }, ) @@ -119,6 +119,7 @@ fun PreviewAttachmentScreen( label = BitwardenString.download_file.asText(), icon = rememberVectorPainter(id = BitwardenDrawable.ic_download), onClick = { viewModel.trySendAction(PreviewAttachmentAction.DownloadClick) }, + testTag = "ErrorStateDownloadButton", ), modifier = Modifier.fillMaxSize(), ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreenTest.kt index 69e2171181..b055895bbe 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsScreenTest.kt @@ -32,6 +32,7 @@ import org.junit.Test class AttachmentsScreenTest : BitwardenComposeTest() { private var onNavigateBackCalled = false + private var onNavigateToPreviewCalled = false private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val mutableEventFlow = bufferedMutableSharedFlow() @@ -50,6 +51,7 @@ class AttachmentsScreenTest : BitwardenComposeTest() { AttachmentsScreen( viewModel = viewModel, onNavigateBack = { onNavigateBackCalled = true }, + onNavigateToPreview = { _, _, _ -> onNavigateToPreviewCalled = true }, ) } } @@ -60,6 +62,18 @@ class AttachmentsScreenTest : BitwardenComposeTest() { assertTrue(onNavigateBackCalled) } + @Test + fun `NavigateToPreview should call onNavigateToPreview`() { + mutableEventFlow.tryEmit( + AttachmentsEvent.NavigateToPreview( + cipherId = "cipherId", + attachmentId = "attachmentId", + fileName = "file.png", + ), + ) + assertTrue(onNavigateToPreviewCalled) + } + @Test fun `on back click should send BackClick`() { composeTestRule.onNodeWithContentDescription("Back").performClick() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt index cf7cfda3fa..90ab0481cb 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +@Suppress("LargeClass") class AttachmentsViewModelTest : BaseViewModelTest() { private val mutableUserStateFlow = MutableStateFlow(DEFAULT_USER_STATE) private val authRepository: AuthRepository = mockk { @@ -85,6 +86,24 @@ class AttachmentsViewModelTest : BaseViewModelTest() { } } + @Test + fun `ItemClick should emit NavigateToPreview`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction( + AttachmentsAction.ItemClick(attachment = DEFAULT_ATTACHMENT_ITEM), + ) + assertEquals( + AttachmentsEvent.NavigateToPreview( + cipherId = DEFAULT_STATE.cipherId, + attachmentId = DEFAULT_ATTACHMENT_ITEM.id, + fileName = DEFAULT_ATTACHMENT_ITEM.title, + ), + awaitItem(), + ) + } + } + @Test fun `SaveClick should display error dialog when user is not Premium`() = runTest { val cipherView = createMockCipherView(number = 1) @@ -721,16 +740,17 @@ private val DEFAULT_STATE: AttachmentsState = AttachmentsState( isPremiumUser = true, ) +private val DEFAULT_ATTACHMENT_ITEM: AttachmentsState.AttachmentItem = + AttachmentsState.AttachmentItem( + id = "mockId-1", + title = "mockFileName-1", + displaySize = "mockSizeName-1", + ) + private val DEFAULT_CONTENT_WITH_ATTACHMENTS: AttachmentsState.ViewState.Content = AttachmentsState.ViewState.Content( originalCipher = createMockCipherView(number = 1), - attachments = listOf( - AttachmentsState.AttachmentItem( - id = "mockId-1", - title = "mockFileName-1", - displaySize = "mockSizeName-1", - ), - ), + attachments = listOf(DEFAULT_ATTACHMENT_ITEM), newAttachment = null, )