mirror of
https://github.com/bitwarden/android.git
synced 2026-08-18 10:17:26 -05:00
PM-34544: bug: Handle large attachments in preview (#6757)
This commit is contained in:
+2
-14
@@ -203,13 +203,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
|||||||
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = it),
|
passwordHistoryMode = GeneratorPasswordHistoryMode.Item(itemId = it),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onNavigateToPreviewAttachment = { cipherId, attachmentId, fileName ->
|
onNavigateToPreviewAttachment = { navController.navigateToPreviewAttachment(it) },
|
||||||
navController.navigateToPreviewAttachment(
|
|
||||||
cipherId = cipherId,
|
|
||||||
attachmentId = attachmentId,
|
|
||||||
fileName = fileName,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
vaultQrCodeScanDestination(
|
vaultQrCodeScanDestination(
|
||||||
onNavigateToManualCodeEntryScreen = {
|
onNavigateToManualCodeEntryScreen = {
|
||||||
@@ -260,13 +254,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
|||||||
)
|
)
|
||||||
attachmentDestination(
|
attachmentDestination(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onNavigateToPreviewAttachment = { cipherId, attachmentId, fileName ->
|
onNavigateToPreviewAttachment = { navController.navigateToPreviewAttachment(it) },
|
||||||
navController.navigateToPreviewAttachment(
|
|
||||||
cipherId = cipherId,
|
|
||||||
attachmentId = attachmentId,
|
|
||||||
fileName = fileName,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
setupUnlockDestination(
|
setupUnlockDestination(
|
||||||
onNavigateBack = {
|
onNavigateBack = {
|
||||||
|
|||||||
+2
-5
@@ -6,6 +6,7 @@ import androidx.navigation.NavGraphBuilder
|
|||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,11 +35,7 @@ fun SavedStateHandle.toAttachmentsArgs(): AttachmentsArgs {
|
|||||||
*/
|
*/
|
||||||
fun NavGraphBuilder.attachmentDestination(
|
fun NavGraphBuilder.attachmentDestination(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToPreviewAttachment: (
|
onNavigateToPreviewAttachment: (route: PreviewAttachmentRoute) -> Unit,
|
||||||
cipherId: String,
|
|
||||||
attachmentId: String,
|
|
||||||
fileName: String,
|
|
||||||
) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions<AttachmentsRoute> {
|
composableWithSlideTransitions<AttachmentsRoute> {
|
||||||
AttachmentsScreen(
|
AttachmentsScreen(
|
||||||
|
|||||||
+11
-2
@@ -32,6 +32,7 @@ import com.bitwarden.ui.platform.manager.IntentManager
|
|||||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.attachments.handlers.AttachmentsHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.handlers.AttachmentsHandlers
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the attachments screen.
|
* Displays the attachments screen.
|
||||||
@@ -43,7 +44,7 @@ fun AttachmentsScreen(
|
|||||||
viewModel: AttachmentsViewModel = hiltViewModel(),
|
viewModel: AttachmentsViewModel = hiltViewModel(),
|
||||||
intentManager: IntentManager = LocalIntentManager.current,
|
intentManager: IntentManager = LocalIntentManager.current,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToPreview: (cipherId: String, attachmentId: String, fileName: String) -> Unit,
|
onNavigateToPreview: (route: PreviewAttachmentRoute) -> Unit,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
val attachmentsHandlers = remember(viewModel) { AttachmentsHandlers.create(viewModel) }
|
val attachmentsHandlers = remember(viewModel) { AttachmentsHandlers.create(viewModel) }
|
||||||
@@ -65,7 +66,15 @@ fun AttachmentsScreen(
|
|||||||
|
|
||||||
is AttachmentsEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
|
is AttachmentsEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
|
||||||
is AttachmentsEvent.NavigateToPreview -> {
|
is AttachmentsEvent.NavigateToPreview -> {
|
||||||
onNavigateToPreview(event.cipherId, event.attachmentId, event.fileName)
|
onNavigateToPreview(
|
||||||
|
PreviewAttachmentRoute(
|
||||||
|
cipherId = event.cipherId,
|
||||||
|
attachmentId = event.attachmentId,
|
||||||
|
fileName = event.fileName,
|
||||||
|
displaySize = event.displaySize,
|
||||||
|
isLargeFile = event.isLargeFile,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -247,6 +247,8 @@ class AttachmentsViewModel @Inject constructor(
|
|||||||
cipherId = state.cipherId,
|
cipherId = state.cipherId,
|
||||||
attachmentId = action.attachment.id,
|
attachmentId = action.attachment.id,
|
||||||
fileName = action.attachment.title,
|
fileName = action.attachment.title,
|
||||||
|
displaySize = action.attachment.displaySize,
|
||||||
|
isLargeFile = action.attachment.isLargeFile,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -474,6 +476,7 @@ data class AttachmentsState(
|
|||||||
val id: String,
|
val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val displaySize: String,
|
val displaySize: String,
|
||||||
|
val isLargeFile: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -527,6 +530,8 @@ sealed class AttachmentsEvent {
|
|||||||
val cipherId: String,
|
val cipherId: String,
|
||||||
val attachmentId: String,
|
val attachmentId: String,
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
|
val displaySize: String,
|
||||||
|
val isLargeFile: Boolean,
|
||||||
) : AttachmentsEvent()
|
) : AttachmentsEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+8
-8
@@ -16,6 +16,8 @@ data class PreviewAttachmentRoute(
|
|||||||
val cipherId: String,
|
val cipherId: String,
|
||||||
val attachmentId: String,
|
val attachmentId: String,
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
|
val displaySize: String,
|
||||||
|
val isLargeFile: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +27,8 @@ data class PreviewAttachmentArgs(
|
|||||||
val cipherId: String,
|
val cipherId: String,
|
||||||
val attachmentId: String,
|
val attachmentId: String,
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
|
val displaySize: String,
|
||||||
|
val isLargeFile: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +40,8 @@ fun SavedStateHandle.toPreviewAttachmentArgs(): PreviewAttachmentArgs {
|
|||||||
cipherId = route.cipherId,
|
cipherId = route.cipherId,
|
||||||
attachmentId = route.attachmentId,
|
attachmentId = route.attachmentId,
|
||||||
fileName = route.fileName,
|
fileName = route.fileName,
|
||||||
|
displaySize = route.displaySize,
|
||||||
|
isLargeFile = route.isLargeFile,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,17 +62,11 @@ fun NavGraphBuilder.previewAttachmentDestination(
|
|||||||
* Navigate to the preview attachment screen.
|
* Navigate to the preview attachment screen.
|
||||||
*/
|
*/
|
||||||
fun NavController.navigateToPreviewAttachment(
|
fun NavController.navigateToPreviewAttachment(
|
||||||
cipherId: String,
|
route: PreviewAttachmentRoute,
|
||||||
attachmentId: String,
|
|
||||||
fileName: String,
|
|
||||||
navOptions: NavOptions? = null,
|
navOptions: NavOptions? = null,
|
||||||
) {
|
) {
|
||||||
navigate(
|
navigate(
|
||||||
route = PreviewAttachmentRoute(
|
route = route,
|
||||||
cipherId = cipherId,
|
|
||||||
attachmentId = attachmentId,
|
|
||||||
fileName = fileName,
|
|
||||||
),
|
|
||||||
navOptions = navOptions,
|
navOptions = navOptions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+27
@@ -11,6 +11,8 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
||||||
|
import com.bitwarden.ui.platform.base.util.spanStyleOf
|
||||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||||
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
||||||
@@ -19,6 +21,7 @@ import com.bitwarden.ui.platform.components.content.BitwardenErrorContent
|
|||||||
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
|
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
|
||||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||||
|
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||||
import com.bitwarden.ui.platform.components.preview.ImagePreviewContent
|
import com.bitwarden.ui.platform.components.preview.ImagePreviewContent
|
||||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||||
@@ -29,6 +32,7 @@ import com.bitwarden.ui.platform.composition.LocalIntentManager
|
|||||||
import com.bitwarden.ui.platform.manager.IntentManager
|
import com.bitwarden.ui.platform.manager.IntentManager
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||||
|
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
import com.bitwarden.ui.util.asText
|
import com.bitwarden.ui.util.asText
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,6 +69,9 @@ fun PreviewAttachmentScreen(
|
|||||||
dialogState = state.dialogState,
|
dialogState = state.dialogState,
|
||||||
onDismissRequest = { viewModel.trySendAction(PreviewAttachmentAction.DismissDialog) },
|
onDismissRequest = { viewModel.trySendAction(PreviewAttachmentAction.DismissDialog) },
|
||||||
onCloseClick = { viewModel.trySendAction(PreviewAttachmentAction.CloseClick) },
|
onCloseClick = { viewModel.trySendAction(PreviewAttachmentAction.CloseClick) },
|
||||||
|
onConfirmDownloadClick = {
|
||||||
|
viewModel.trySendAction(PreviewAttachmentAction.ConfirmDownloadClick)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
@@ -139,6 +146,7 @@ private fun PreviewAttachmentDialogs(
|
|||||||
dialogState: PreviewAttachmentState.DialogState?,
|
dialogState: PreviewAttachmentState.DialogState?,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onCloseClick: () -> Unit,
|
onCloseClick: () -> Unit,
|
||||||
|
onConfirmDownloadClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
when (dialogState) {
|
when (dialogState) {
|
||||||
is PreviewAttachmentState.DialogState.Error -> {
|
is PreviewAttachmentState.DialogState.Error -> {
|
||||||
@@ -165,6 +173,25 @@ private fun PreviewAttachmentDialogs(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is PreviewAttachmentState.DialogState.DownloadLargeFileConfirmation -> {
|
||||||
|
BitwardenTwoButtonDialog(
|
||||||
|
title = stringResource(id = BitwardenString.download_attachment),
|
||||||
|
message = annotatedStringResource(
|
||||||
|
id = BitwardenString.attachment_large_warning,
|
||||||
|
args = arrayOf(dialogState.displaySize),
|
||||||
|
style = spanStyleOf(
|
||||||
|
color = BitwardenTheme.colorScheme.text.primary,
|
||||||
|
textStyle = BitwardenTheme.typography.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
confirmButtonText = stringResource(id = BitwardenString.yes),
|
||||||
|
dismissButtonText = stringResource(id = BitwardenString.no),
|
||||||
|
onConfirmClick = onConfirmDownloadClick,
|
||||||
|
onDismissClick = onDismissRequest,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
null -> Unit
|
null -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-4
@@ -53,19 +53,27 @@ class PreviewAttachmentViewModel @Inject constructor(
|
|||||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||||
val args = savedStateHandle.toPreviewAttachmentArgs()
|
val args = savedStateHandle.toPreviewAttachmentArgs()
|
||||||
val isPreviewable = args.fileName.isPreviewable
|
val isPreviewable = args.fileName.isPreviewable
|
||||||
|
val isLargeFile = args.isLargeFile
|
||||||
PreviewAttachmentState(
|
PreviewAttachmentState(
|
||||||
cipherId = args.cipherId,
|
cipherId = args.cipherId,
|
||||||
attachmentId = args.attachmentId,
|
attachmentId = args.attachmentId,
|
||||||
fileName = args.fileName,
|
fileName = args.fileName,
|
||||||
|
displaySize = args.displaySize,
|
||||||
|
isLargeFile = isLargeFile,
|
||||||
isPreviewable = isPreviewable,
|
isPreviewable = isPreviewable,
|
||||||
viewState = if (isPreviewable) {
|
viewState = if (!isPreviewable) {
|
||||||
PreviewAttachmentState.ViewState.Loading()
|
|
||||||
} else {
|
|
||||||
PreviewAttachmentState.ViewState.Error(
|
PreviewAttachmentState.ViewState.Error(
|
||||||
message = BitwardenString
|
message = BitwardenString
|
||||||
.preview_not_available_for_files
|
.preview_not_available_for_files
|
||||||
.asText(args.fileName.fileExtension),
|
.asText(args.fileName.fileExtension),
|
||||||
)
|
)
|
||||||
|
} else if (isLargeFile) {
|
||||||
|
PreviewAttachmentState.ViewState.Error(
|
||||||
|
message = BitwardenString.this_file_is_too_large_to_preview.asText(),
|
||||||
|
illustrationRes = BitwardenDrawable.ill_file_error,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PreviewAttachmentState.ViewState.Loading()
|
||||||
},
|
},
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
)
|
)
|
||||||
@@ -88,7 +96,7 @@ class PreviewAttachmentViewModel @Inject constructor(
|
|||||||
refreshDataFlow
|
refreshDataFlow
|
||||||
.filter {
|
.filter {
|
||||||
// Don't bother retrieving the file is we cannot display it.
|
// Don't bother retrieving the file is we cannot display it.
|
||||||
state.isPreviewable
|
state.isPreviewable && !state.isLargeFile
|
||||||
}
|
}
|
||||||
.flatMapLatest {
|
.flatMapLatest {
|
||||||
vaultRepository
|
vaultRepository
|
||||||
@@ -106,6 +114,7 @@ class PreviewAttachmentViewModel @Inject constructor(
|
|||||||
PreviewAttachmentAction.CloseClick -> handleCloseClick()
|
PreviewAttachmentAction.CloseClick -> handleCloseClick()
|
||||||
PreviewAttachmentAction.DismissDialog -> handleDismissDialog()
|
PreviewAttachmentAction.DismissDialog -> handleDismissDialog()
|
||||||
PreviewAttachmentAction.DownloadClick -> handleDownloadClick()
|
PreviewAttachmentAction.DownloadClick -> handleDownloadClick()
|
||||||
|
PreviewAttachmentAction.ConfirmDownloadClick -> handleConfirmDownloadClick()
|
||||||
PreviewAttachmentAction.BitmapRenderComplete -> handleBitmapRenderComplete()
|
PreviewAttachmentAction.BitmapRenderComplete -> handleBitmapRenderComplete()
|
||||||
PreviewAttachmentAction.BitmapRenderError -> handleBitmapRenderError()
|
PreviewAttachmentAction.BitmapRenderError -> handleBitmapRenderError()
|
||||||
PreviewAttachmentAction.FileMissing -> handleFileMissing()
|
PreviewAttachmentAction.FileMissing -> handleFileMissing()
|
||||||
@@ -316,6 +325,24 @@ class PreviewAttachmentViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDownloadClick() {
|
private fun handleDownloadClick() {
|
||||||
|
if (state.isLargeFile) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = PreviewAttachmentState.DialogState.DownloadLargeFileConfirmation(
|
||||||
|
displaySize = state.displaySize,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
downloadFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleConfirmDownloadClick() {
|
||||||
|
downloadFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadFile() {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
dialogState = PreviewAttachmentState.DialogState.Loading(
|
dialogState = PreviewAttachmentState.DialogState.Loading(
|
||||||
@@ -390,6 +417,8 @@ data class PreviewAttachmentState(
|
|||||||
val cipherId: String,
|
val cipherId: String,
|
||||||
val attachmentId: String,
|
val attachmentId: String,
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
|
val displaySize: String,
|
||||||
|
val isLargeFile: Boolean,
|
||||||
val isPreviewable: Boolean,
|
val isPreviewable: Boolean,
|
||||||
val viewState: ViewState,
|
val viewState: ViewState,
|
||||||
val dialogState: DialogState?,
|
val dialogState: DialogState?,
|
||||||
@@ -452,6 +481,14 @@ data class PreviewAttachmentState(
|
|||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object PreviewUnavailable : DialogState()
|
data object PreviewUnavailable : DialogState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a warning dialog when the user is about to download a large file.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class DownloadLargeFileConfirmation(
|
||||||
|
val displaySize: String,
|
||||||
|
) : DialogState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,6 +554,11 @@ sealed class PreviewAttachmentAction {
|
|||||||
*/
|
*/
|
||||||
data object DownloadClick : PreviewAttachmentAction()
|
data object DownloadClick : PreviewAttachmentAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User clicked to confirm that they want to download the file.
|
||||||
|
*/
|
||||||
|
data object ConfirmDownloadClick : PreviewAttachmentAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bitmap has been rendered from file.
|
* The bitmap has been rendered from file.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package com.x8bit.bitwarden.ui.vault.feature.attachments.util
|
||||||
|
|
||||||
|
import com.bitwarden.vault.AttachmentView
|
||||||
|
|
||||||
|
private const val TEN_MB_IN_BYTES: Long = 10485760L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return `true` if the file is larger than 10MB, `false` otherwise.
|
||||||
|
*/
|
||||||
|
fun AttachmentView.isLargeFile(): Boolean =
|
||||||
|
try {
|
||||||
|
(this.size?.toLong() ?: 0L) >= TEN_MB_IN_BYTES
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
+1
@@ -19,6 +19,7 @@ fun CipherView.toViewState(): AttachmentsState.ViewState.Content =
|
|||||||
id = id,
|
id = id,
|
||||||
title = it.fileName.orEmpty(),
|
title = it.fileName.orEmpty(),
|
||||||
displaySize = it.sizeName.orEmpty(),
|
displaySize = it.sizeName.orEmpty(),
|
||||||
|
isLargeFile = it.isLargeFile(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toImmutableList(),
|
.toImmutableList(),
|
||||||
|
|||||||
+2
-5
@@ -7,6 +7,7 @@ import androidx.navigation.NavOptions
|
|||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
|
||||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -45,11 +46,7 @@ fun NavGraphBuilder.vaultItemDestination(
|
|||||||
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
|
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
|
||||||
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
||||||
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
|
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
|
||||||
onNavigateToPreviewAttachment: (
|
onNavigateToPreviewAttachment: (route: PreviewAttachmentRoute) -> Unit,
|
||||||
cipherId: String,
|
|
||||||
attachmentId: String,
|
|
||||||
fileName: String,
|
|
||||||
) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions<VaultItemRoute> {
|
composableWithSlideTransitions<VaultItemRoute> {
|
||||||
VaultItemScreen(
|
VaultItemScreen(
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
|||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||||
import com.bitwarden.ui.util.asText
|
import com.bitwarden.ui.util.asText
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
|
||||||
@@ -63,11 +64,7 @@ fun VaultItemScreen(
|
|||||||
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
|
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
|
||||||
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
||||||
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
|
onNavigateToPasswordHistory: (vaultItemId: String) -> Unit,
|
||||||
onNavigateToPreviewAttachment: (
|
onNavigateToPreviewAttachment: (route: PreviewAttachmentRoute) -> Unit,
|
||||||
cipherId: String,
|
|
||||||
attachmentId: String,
|
|
||||||
fileName: String,
|
|
||||||
) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
val fileChooserLauncher = intentManager.getActivityResultLauncher { activityResult ->
|
val fileChooserLauncher = intentManager.getActivityResultLauncher { activityResult ->
|
||||||
@@ -122,7 +119,15 @@ fun VaultItemScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is VaultItemEvent.NavigateToPreviewAttachment -> {
|
is VaultItemEvent.NavigateToPreviewAttachment -> {
|
||||||
onNavigateToPreviewAttachment(event.cipherId, event.attachmentId, event.fileName)
|
onNavigateToPreviewAttachment(
|
||||||
|
PreviewAttachmentRoute(
|
||||||
|
cipherId = event.cipherId,
|
||||||
|
attachmentId = event.attachmentId,
|
||||||
|
fileName = event.fileName,
|
||||||
|
displaySize = event.displaySize,
|
||||||
|
isLargeFile = event.isLargeFile,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,6 +423,8 @@ class VaultItemViewModel @Inject constructor(
|
|||||||
cipherId = state.vaultItemId,
|
cipherId = state.vaultItemId,
|
||||||
attachmentId = action.attachment.id,
|
attachmentId = action.attachment.id,
|
||||||
fileName = action.attachment.title,
|
fileName = action.attachment.title,
|
||||||
|
displaySize = action.attachment.displaySize,
|
||||||
|
isLargeFile = action.attachment.isLargeFile,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2008,6 +2010,8 @@ sealed class VaultItemEvent {
|
|||||||
val cipherId: String,
|
val cipherId: String,
|
||||||
val attachmentId: String,
|
val attachmentId: String,
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
|
val displaySize: String,
|
||||||
|
val isLargeFile: Boolean,
|
||||||
) : VaultItemEvent()
|
) : VaultItemEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+10
-8
@@ -20,7 +20,9 @@ import androidx.compose.ui.platform.testTag
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitwarden.ui.platform.base.util.annotatedStringResource
|
||||||
import com.bitwarden.ui.platform.base.util.cardStyle
|
import com.bitwarden.ui.platform.base.util.cardStyle
|
||||||
|
import com.bitwarden.ui.platform.base.util.spanStyleOf
|
||||||
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
||||||
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||||
@@ -57,10 +59,6 @@ fun VaultItemAttachment(
|
|||||||
shouldShowPremiumWarningDialog = true
|
shouldShowPremiumWarningDialog = true
|
||||||
return@cardStyle
|
return@cardStyle
|
||||||
}
|
}
|
||||||
if (attachmentItem.isLargeFile) {
|
|
||||||
shouldShowSizeWarningDialog = true
|
|
||||||
return@cardStyle
|
|
||||||
}
|
|
||||||
onAttachmentPreviewClick(attachmentItem)
|
onAttachmentPreviewClick(attachmentItem)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -138,10 +136,14 @@ fun VaultItemAttachment(
|
|||||||
|
|
||||||
if (shouldShowSizeWarningDialog) {
|
if (shouldShowSizeWarningDialog) {
|
||||||
BitwardenTwoButtonDialog(
|
BitwardenTwoButtonDialog(
|
||||||
title = null,
|
title = stringResource(id = BitwardenString.download_attachment),
|
||||||
message = stringResource(
|
message = annotatedStringResource(
|
||||||
BitwardenString.attachment_large_warning,
|
id = BitwardenString.attachment_large_warning,
|
||||||
attachmentItem.displaySize,
|
args = arrayOf(attachmentItem.displaySize),
|
||||||
|
style = spanStyleOf(
|
||||||
|
color = BitwardenTheme.colorScheme.text.primary,
|
||||||
|
textStyle = BitwardenTheme.typography.bodyMedium,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
confirmButtonText = stringResource(BitwardenString.yes),
|
confirmButtonText = stringResource(BitwardenString.yes),
|
||||||
dismissButtonText = stringResource(BitwardenString.no),
|
dismissButtonText = stringResource(BitwardenString.no),
|
||||||
|
|||||||
+2
-5
@@ -19,6 +19,7 @@ import com.bitwarden.vault.FieldView
|
|||||||
import com.bitwarden.vault.IdentityView
|
import com.bitwarden.vault.IdentityView
|
||||||
import com.bitwarden.vault.LoginUriView
|
import com.bitwarden.vault.LoginUriView
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.util.isLargeFile
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
|
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
|
||||||
@@ -97,11 +98,7 @@ fun CipherView.toViewState(
|
|||||||
title = requireNotNull(it.fileName),
|
title = requireNotNull(it.fileName),
|
||||||
displaySize = requireNotNull(it.sizeName),
|
displaySize = requireNotNull(it.sizeName),
|
||||||
url = requireNotNull(it.url),
|
url = requireNotNull(it.url),
|
||||||
isLargeFile = try {
|
isLargeFile = it.isLargeFile(),
|
||||||
requireNotNull(it.size).toLong() >= 10485760
|
|
||||||
} catch (_: NumberFormatException) {
|
|
||||||
false
|
|
||||||
},
|
|
||||||
isDownloadAllowed = isPremiumUser || this.organizationId != null,
|
isDownloadAllowed = isPremiumUser || this.organizationId != null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -54,7 +54,7 @@ class AttachmentsScreenTest : BitwardenComposeTest() {
|
|||||||
AttachmentsScreen(
|
AttachmentsScreen(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
onNavigateToPreview = { _, _, _ -> onNavigateToPreviewCalled = true },
|
onNavigateToPreview = { onNavigateToPreviewCalled = true },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,8 @@ class AttachmentsScreenTest : BitwardenComposeTest() {
|
|||||||
cipherId = "cipherId",
|
cipherId = "cipherId",
|
||||||
attachmentId = "attachmentId",
|
attachmentId = "attachmentId",
|
||||||
fileName = "file.png",
|
fileName = "file.png",
|
||||||
|
displaySize = "10 MB",
|
||||||
|
isLargeFile = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertTrue(onNavigateToPreviewCalled)
|
assertTrue(onNavigateToPreviewCalled)
|
||||||
@@ -337,6 +339,7 @@ private val DEFAULT_CONTENT_WITH_ATTACHMENTS: AttachmentsState.ViewState.Content
|
|||||||
id = "cipherId-1234",
|
id = "cipherId-1234",
|
||||||
title = "cool_file.png",
|
title = "cool_file.png",
|
||||||
displaySize = "10 MB",
|
displaySize = "10 MB",
|
||||||
|
isLargeFile = true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
newAttachment = null,
|
newAttachment = null,
|
||||||
|
|||||||
+3
@@ -137,6 +137,8 @@ class AttachmentsViewModelTest : BaseViewModelTest() {
|
|||||||
cipherId = DEFAULT_STATE.cipherId,
|
cipherId = DEFAULT_STATE.cipherId,
|
||||||
attachmentId = DEFAULT_ATTACHMENT_ITEM.id,
|
attachmentId = DEFAULT_ATTACHMENT_ITEM.id,
|
||||||
fileName = DEFAULT_ATTACHMENT_ITEM.title,
|
fileName = DEFAULT_ATTACHMENT_ITEM.title,
|
||||||
|
displaySize = DEFAULT_ATTACHMENT_ITEM.displaySize,
|
||||||
|
isLargeFile = false,
|
||||||
),
|
),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
@@ -850,6 +852,7 @@ private val DEFAULT_ATTACHMENT_ITEM: AttachmentsState.AttachmentItem =
|
|||||||
id = "mockId-1",
|
id = "mockId-1",
|
||||||
title = "mockFileName-1",
|
title = "mockFileName-1",
|
||||||
displaySize = "mockSizeName-1",
|
displaySize = "mockSizeName-1",
|
||||||
|
isLargeFile = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_CONTENT_WITH_ATTACHMENTS: AttachmentsState.ViewState.Content =
|
private val DEFAULT_CONTENT_WITH_ATTACHMENTS: AttachmentsState.ViewState.Content =
|
||||||
|
|||||||
+31
@@ -247,6 +247,35 @@ class PreviewAttachmentScreenTest : BitwardenComposeTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `confirm download dialog should display title and message`() {
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = PreviewAttachmentState.DialogState.DownloadLargeFileConfirmation(
|
||||||
|
displaySize = "2.89 MB",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Download Attachment")
|
||||||
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "This file is 2.89 MB. Would you like to download it?")
|
||||||
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(text = "Yes")
|
||||||
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
|
.performClick()
|
||||||
|
verify(exactly = 1) {
|
||||||
|
viewModel.trySendAction(PreviewAttachmentAction.ConfirmDownloadClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `NavigateToSelectAttachmentSaveLocation event should launch file chooser`() {
|
fun `NavigateToSelectAttachmentSaveLocation event should launch file chooser`() {
|
||||||
val fileName = "test.png"
|
val fileName = "test.png"
|
||||||
@@ -265,6 +294,8 @@ private val DEFAULT_STATE = PreviewAttachmentState(
|
|||||||
cipherId = "mockCipherId",
|
cipherId = "mockCipherId",
|
||||||
attachmentId = "mockAttachmentId",
|
attachmentId = "mockAttachmentId",
|
||||||
fileName = DEFAULT_FILE_NAME,
|
fileName = DEFAULT_FILE_NAME,
|
||||||
|
displaySize = "2.89 MB",
|
||||||
|
isLargeFile = false,
|
||||||
isPreviewable = true,
|
isPreviewable = true,
|
||||||
viewState = PreviewAttachmentState.ViewState.Loading(),
|
viewState = PreviewAttachmentState.ViewState.Loading(),
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
|
|||||||
+54
@@ -304,6 +304,53 @@ class PreviewAttachmentViewModelTest : BaseViewModelTest() {
|
|||||||
coVerify(exactly = 1) { fileManager.delete(mockFile) }
|
coVerify(exactly = 1) { fileManager.delete(mockFile) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `ConfirmDownloadClick should show loading dialog and emit NavigateToSelectAttachmentSaveLocation`() =
|
||||||
|
runTest {
|
||||||
|
val cipherView = createMockCipherView(number = 1)
|
||||||
|
mutableVaultItemStateFlow.value = DataState.Loaded(cipherView)
|
||||||
|
coEvery {
|
||||||
|
vaultRepository.downloadAttachment(
|
||||||
|
cipherView = cipherView,
|
||||||
|
attachmentId = DEFAULT_ATTACHMENT_ID,
|
||||||
|
)
|
||||||
|
} returns DownloadAttachmentResult.Success(mockFile)
|
||||||
|
val viewModel = createViewModel(initialState = NON_PREVIEWABLE_STATE)
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(PreviewAttachmentAction.ConfirmDownloadClick)
|
||||||
|
assertEquals(
|
||||||
|
PreviewAttachmentEvent.NavigateToSelectAttachmentSaveLocation(
|
||||||
|
fileName = NON_PREVIEWABLE_STATE.fileName,
|
||||||
|
),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `DownloadClick should show confirmation dialog when file is large`() = runTest {
|
||||||
|
val cipherView = createMockCipherView(number = 1)
|
||||||
|
mutableVaultItemStateFlow.value = DataState.Loaded(cipherView)
|
||||||
|
coEvery {
|
||||||
|
vaultRepository.downloadAttachment(
|
||||||
|
cipherView = cipherView,
|
||||||
|
attachmentId = DEFAULT_ATTACHMENT_ID,
|
||||||
|
)
|
||||||
|
} returns DownloadAttachmentResult.Success(mockFile)
|
||||||
|
val viewModel = createViewModel(initialState = DEFAULT_STATE.copy(isLargeFile = true))
|
||||||
|
viewModel.trySendAction(PreviewAttachmentAction.DownloadClick)
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialogState = PreviewAttachmentState.DialogState.DownloadLargeFileConfirmation(
|
||||||
|
displaySize = DEFAULT_STATE.displaySize,
|
||||||
|
),
|
||||||
|
isLargeFile = true,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `DownloadClick should show loading dialog and emit NavigateToSelectAttachmentSaveLocation`() =
|
fun `DownloadClick should show loading dialog and emit NavigateToSelectAttachmentSaveLocation`() =
|
||||||
@@ -857,6 +904,8 @@ class PreviewAttachmentViewModelTest : BaseViewModelTest() {
|
|||||||
cipherId = initialState?.cipherId ?: DEFAULT_CIPHER_ID,
|
cipherId = initialState?.cipherId ?: DEFAULT_CIPHER_ID,
|
||||||
attachmentId = initialState?.attachmentId ?: DEFAULT_ATTACHMENT_ID,
|
attachmentId = initialState?.attachmentId ?: DEFAULT_ATTACHMENT_ID,
|
||||||
fileName = initialState?.fileName ?: DEFAULT_FILE_NAME,
|
fileName = initialState?.fileName ?: DEFAULT_FILE_NAME,
|
||||||
|
displaySize = "2.89 MB",
|
||||||
|
isLargeFile = false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -865,11 +914,14 @@ class PreviewAttachmentViewModelTest : BaseViewModelTest() {
|
|||||||
private const val DEFAULT_CIPHER_ID = "mockCipherId"
|
private const val DEFAULT_CIPHER_ID = "mockCipherId"
|
||||||
private const val DEFAULT_ATTACHMENT_ID = "mockAttachmentId"
|
private const val DEFAULT_ATTACHMENT_ID = "mockAttachmentId"
|
||||||
private const val DEFAULT_FILE_NAME = "test.png"
|
private const val DEFAULT_FILE_NAME = "test.png"
|
||||||
|
private const val DEFAULT_DISPLAY_SIZE = "2.89 MB"
|
||||||
|
|
||||||
private val DEFAULT_STATE = PreviewAttachmentState(
|
private val DEFAULT_STATE = PreviewAttachmentState(
|
||||||
cipherId = DEFAULT_CIPHER_ID,
|
cipherId = DEFAULT_CIPHER_ID,
|
||||||
attachmentId = DEFAULT_ATTACHMENT_ID,
|
attachmentId = DEFAULT_ATTACHMENT_ID,
|
||||||
fileName = DEFAULT_FILE_NAME,
|
fileName = DEFAULT_FILE_NAME,
|
||||||
|
displaySize = DEFAULT_DISPLAY_SIZE,
|
||||||
|
isLargeFile = false,
|
||||||
isPreviewable = true,
|
isPreviewable = true,
|
||||||
viewState = PreviewAttachmentState.ViewState.Loading(),
|
viewState = PreviewAttachmentState.ViewState.Loading(),
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
@@ -879,7 +931,9 @@ private val NON_PREVIEWABLE_STATE = PreviewAttachmentState(
|
|||||||
cipherId = DEFAULT_CIPHER_ID,
|
cipherId = DEFAULT_CIPHER_ID,
|
||||||
attachmentId = DEFAULT_ATTACHMENT_ID,
|
attachmentId = DEFAULT_ATTACHMENT_ID,
|
||||||
fileName = "test.pdf",
|
fileName = "test.pdf",
|
||||||
|
displaySize = DEFAULT_DISPLAY_SIZE,
|
||||||
isPreviewable = false,
|
isPreviewable = false,
|
||||||
|
isLargeFile = false,
|
||||||
viewState = PreviewAttachmentState.ViewState.Error(
|
viewState = PreviewAttachmentState.ViewState.Error(
|
||||||
message = BitwardenString.preview_not_available_for_files.asText("PDF"),
|
message = BitwardenString.preview_not_available_for_files.asText("PDF"),
|
||||||
),
|
),
|
||||||
|
|||||||
+1
@@ -23,6 +23,7 @@ class CipherViewExtensionsTest {
|
|||||||
id = "mockId-1",
|
id = "mockId-1",
|
||||||
title = "mockFileName-1",
|
title = "mockFileName-1",
|
||||||
displaySize = "mockSizeName-1",
|
displaySize = "mockSizeName-1",
|
||||||
|
isLargeFile = false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
newAttachment = null,
|
newAttachment = null,
|
||||||
|
|||||||
+16
-15
@@ -41,6 +41,7 @@ import com.bitwarden.ui.util.onNodeWithTextAfterScroll
|
|||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.preview.PreviewAttachmentRoute
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
|
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemLocation
|
||||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||||
@@ -69,7 +70,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
|
|||||||
private var onNavigateToMoveToOrganizationItemId: String? = null
|
private var onNavigateToMoveToOrganizationItemId: String? = null
|
||||||
private var onNavigateToAttachmentsId: String? = null
|
private var onNavigateToAttachmentsId: String? = null
|
||||||
private var onNavigateToPasswordHistoryId: String? = null
|
private var onNavigateToPasswordHistoryId: String? = null
|
||||||
private var onNavigateToPreviewAttachmentId: String? = null
|
private var onNavigateToPreviewAttachment: PreviewAttachmentRoute? = null
|
||||||
|
|
||||||
private val intentManager = mockk<IntentManager>(relaxed = true)
|
private val intentManager = mockk<IntentManager>(relaxed = true)
|
||||||
|
|
||||||
@@ -94,9 +95,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
|
|||||||
},
|
},
|
||||||
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
|
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
|
||||||
onNavigateToPasswordHistory = { onNavigateToPasswordHistoryId = it },
|
onNavigateToPasswordHistory = { onNavigateToPasswordHistoryId = it },
|
||||||
onNavigateToPreviewAttachment = { id, _, _ ->
|
onNavigateToPreviewAttachment = { onNavigateToPreviewAttachment = it },
|
||||||
onNavigateToPreviewAttachmentId = id
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,15 +143,23 @@ class VaultItemScreenTest : BitwardenComposeTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `NavigateToPreviewAttachment event should invoke onNavigateToPreviewAttachment`() {
|
fun `NavigateToPreviewAttachment event should invoke onNavigateToPreviewAttachment`() {
|
||||||
val cipherId = "cipherId1234"
|
val route = PreviewAttachmentRoute(
|
||||||
|
cipherId = "cipherId1234",
|
||||||
|
attachmentId = "attachmentId4321",
|
||||||
|
fileName = "fileName",
|
||||||
|
displaySize = "2.89 MB",
|
||||||
|
isLargeFile = false,
|
||||||
|
)
|
||||||
mutableEventFlow.tryEmit(
|
mutableEventFlow.tryEmit(
|
||||||
VaultItemEvent.NavigateToPreviewAttachment(
|
VaultItemEvent.NavigateToPreviewAttachment(
|
||||||
cipherId = cipherId,
|
cipherId = "cipherId1234",
|
||||||
attachmentId = "attachmentId4321",
|
attachmentId = "attachmentId4321",
|
||||||
fileName = "fileName",
|
fileName = "fileName",
|
||||||
|
displaySize = "2.89 MB",
|
||||||
|
isLargeFile = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertEquals(cipherId, onNavigateToPreviewAttachmentId)
|
assertEquals(route, onNavigateToPreviewAttachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -831,10 +838,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
|
|||||||
.performClick()
|
.performClick()
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onAllNodesWithText(
|
.onAllNodesWithText("This file is 11 MB. Would you like to download it?")
|
||||||
"This attachment is 11 MB in size. Are you sure you want to download it onto " +
|
|
||||||
"your device?",
|
|
||||||
)
|
|
||||||
.filterToOne(hasAnyAncestor(isDialog()))
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
|
||||||
@@ -873,10 +877,7 @@ class VaultItemScreenTest : BitwardenComposeTest() {
|
|||||||
.performClick()
|
.performClick()
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onAllNodesWithText(
|
.onAllNodesWithText("This file is 11 MB. Would you like to download it?")
|
||||||
"This attachment is 11 MB in size. Are you sure you want to download it onto " +
|
|
||||||
"your device?",
|
|
||||||
)
|
|
||||||
.filterToOne(hasAnyAncestor(isDialog()))
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
|||||||
+46
-1
@@ -22,9 +22,11 @@ import androidx.compose.ui.platform.testTag
|
|||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.testTag
|
import androidx.compose.ui.semantics.testTag
|
||||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import com.bitwarden.ui.platform.base.util.toAnnotatedString
|
||||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||||
import com.bitwarden.ui.platform.components.dialog.util.maxDialogHeight
|
import com.bitwarden.ui.platform.components.dialog.util.maxDialogHeight
|
||||||
import com.bitwarden.ui.platform.components.dialog.util.maxDialogWidth
|
import com.bitwarden.ui.platform.components.dialog.util.maxDialogWidth
|
||||||
@@ -46,7 +48,6 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
|
|||||||
* @param dismissTextColor The color of the dismiss text.
|
* @param dismissTextColor The color of the dismiss text.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@Suppress("LongMethod")
|
|
||||||
fun BitwardenTwoButtonDialog(
|
fun BitwardenTwoButtonDialog(
|
||||||
title: String?,
|
title: String?,
|
||||||
message: String,
|
message: String,
|
||||||
@@ -59,6 +60,50 @@ fun BitwardenTwoButtonDialog(
|
|||||||
dismissTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foreground,
|
dismissTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foreground,
|
||||||
dismissOnBackPress: Boolean = true,
|
dismissOnBackPress: Boolean = true,
|
||||||
dismissOnClickOutside: Boolean = true,
|
dismissOnClickOutside: Boolean = true,
|
||||||
|
) {
|
||||||
|
BitwardenTwoButtonDialog(
|
||||||
|
title = title,
|
||||||
|
message = message.toAnnotatedString(),
|
||||||
|
confirmButtonText = confirmButtonText,
|
||||||
|
dismissButtonText = dismissButtonText,
|
||||||
|
onConfirmClick = onConfirmClick,
|
||||||
|
onDismissClick = onDismissClick,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmTextColor = confirmTextColor,
|
||||||
|
dismissTextColor = dismissTextColor,
|
||||||
|
dismissOnBackPress = dismissOnBackPress,
|
||||||
|
dismissOnClickOutside = dismissOnClickOutside,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Bitwarden-styled dialog with two buttons.
|
||||||
|
*
|
||||||
|
* @param title the optional title to show.
|
||||||
|
* @param message message to show.
|
||||||
|
* @param confirmButtonText text to show on confirm button.
|
||||||
|
* @param dismissButtonText text to show on dismiss button.
|
||||||
|
* @param onConfirmClick called when the confirm button is clicked.
|
||||||
|
* @param onDismissClick called when the dismiss button is clicked.
|
||||||
|
* @param onDismissRequest called when the user attempts to dismiss the dialog (for example by
|
||||||
|
* tapping outside of it).
|
||||||
|
* @param confirmTextColor The color of the confirm text.
|
||||||
|
* @param dismissTextColor The color of the dismiss text.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
fun BitwardenTwoButtonDialog(
|
||||||
|
title: String?,
|
||||||
|
message: AnnotatedString,
|
||||||
|
confirmButtonText: String,
|
||||||
|
dismissButtonText: String,
|
||||||
|
onConfirmClick: () -> Unit,
|
||||||
|
onDismissClick: () -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
confirmTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foreground,
|
||||||
|
dismissTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foreground,
|
||||||
|
dismissOnBackPress: Boolean = true,
|
||||||
|
dismissOnClickOutside: Boolean = true,
|
||||||
) {
|
) {
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
|||||||
@@ -189,7 +189,7 @@
|
|||||||
<string name="attachments">Attachments</string>
|
<string name="attachments">Attachments</string>
|
||||||
<string name="unable_to_download_file">Unable to download file.</string>
|
<string name="unable_to_download_file">Unable to download file.</string>
|
||||||
<string name="downloading">Downloading…</string>
|
<string name="downloading">Downloading…</string>
|
||||||
<string name="attachment_large_warning">This attachment is %1$s in size. Are you sure you want to download it onto your device?</string>
|
<string name="attachment_large_warning">This file is <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation>. Would you like to download it?</string>
|
||||||
<string name="authenticator_key">Authenticator key</string>
|
<string name="authenticator_key">Authenticator key</string>
|
||||||
<string name="verification_code_totp">Verification code (TOTP)</string>
|
<string name="verification_code_totp">Verification code (TOTP)</string>
|
||||||
<string name="authenticator_key_added">Authenticator key added.</string>
|
<string name="authenticator_key_added">Authenticator key added.</string>
|
||||||
@@ -1231,9 +1231,11 @@ Do you want to switch to this account?</string>
|
|||||||
<string name="external_link">External link</string>
|
<string name="external_link">External link</string>
|
||||||
<string name="external_link_format" comment="Used for accessibility to indicate that tapping this item will leave the app">%1$s, External link</string>
|
<string name="external_link_format" comment="Used for accessibility to indicate that tapping this item will leave the app">%1$s, External link</string>
|
||||||
<string name="preview_not_available_for_files">Preview unavailable for %1$s files. You can still download it to view on your device.</string>
|
<string name="preview_not_available_for_files">Preview unavailable for %1$s files. You can still download it to view on your device.</string>
|
||||||
|
<string name="this_file_is_too_large_to_preview">This file is too large to preview. You can still download it to view on your device.</string>
|
||||||
<string name="bitwarden_could_not_decrypt_this_file_so_the_preview_cannot_be_displayed">Bitwarden could not decrypt this file, so the preview cannot be displayed.</string>
|
<string name="bitwarden_could_not_decrypt_this_file_so_the_preview_cannot_be_displayed">Bitwarden could not decrypt this file, so the preview cannot be displayed.</string>
|
||||||
<string name="preview_unavailable_for_this_file">Preview unavailable for this file. You can still download it to view on your device.</string>
|
<string name="preview_unavailable_for_this_file">Preview unavailable for this file. You can still download it to view on your device.</string>
|
||||||
<string name="preview_unavailable">Preview unavailable</string>
|
<string name="preview_unavailable">Preview unavailable</string>
|
||||||
|
<string name="download_attachment">Download Attachment</string>
|
||||||
<string name="upgraded_to_premium">Upgraded to premium</string>
|
<string name="upgraded_to_premium">Upgraded to premium</string>
|
||||||
<string name="unlock_premium_features">Unlock more advanced features with a Premium plan.</string>
|
<string name="unlock_premium_features">Unlock more advanced features with a Premium plan.</string>
|
||||||
<string name="per_month">/ month</string>
|
<string name="per_month">/ month</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user