mirror of
https://github.com/bitwarden/android.git
synced 2026-08-29 10:17:56 -05:00
PM-34042: feat: Preview attachments from AttachmentsScreen (#6712)
This commit is contained in:
+7
@@ -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 = {
|
||||
|
||||
+7
-1
@@ -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,
|
||||
) {
|
||||
|
||||
+6
@@ -34,10 +34,16 @@ fun SavedStateHandle.toAttachmentsArgs(): AttachmentsArgs {
|
||||
*/
|
||||
fun NavGraphBuilder.attachmentDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToPreviewAttachment: (
|
||||
cipherId: String,
|
||||
attachmentId: String,
|
||||
fileName: String,
|
||||
) -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<AttachmentsRoute> {
|
||||
AttachmentsScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToPreview = onNavigateToPreviewAttachment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
-1
@@ -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.
|
||||
*/
|
||||
|
||||
+5
-6
@@ -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)
|
||||
},
|
||||
|
||||
+2
-1
@@ -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(),
|
||||
)
|
||||
|
||||
+14
@@ -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<AttachmentsEvent>()
|
||||
@@ -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()
|
||||
|
||||
+27
-7
@@ -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<UserState?>(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,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user