Add support for deleting an attachment (#768)

This commit is contained in:
David Perez
2024-01-24 19:10:22 -06:00
committed by Álison Fernandes
parent dc3081c5d6
commit 04d60a50ff
7 changed files with 266 additions and 17 deletions

View File

@@ -18,11 +18,15 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.NavigationIcon
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
@@ -65,6 +69,11 @@ fun AttachmentsScreen(
}
}
AttachmentsDialogs(
dialogState = state.dialogState,
onDismissRequest = attachmentsHandlers.onDismissRequest,
)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
@@ -109,3 +118,25 @@ fun AttachmentsScreen(
}
}
}
@Composable
private fun AttachmentsDialogs(
dialogState: AttachmentsState.DialogState?,
onDismissRequest: () -> Unit,
) {
when (dialogState) {
is AttachmentsState.DialogState.Error -> BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialogState.title,
message = dialogState.message,
),
onDismissRequest = onDismissRequest,
)
is AttachmentsState.DialogState.Loading -> BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(dialogState.message),
)
null -> Unit
}
}

View File

@@ -7,6 +7,7 @@ import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
@@ -18,6 +19,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
@@ -26,6 +29,7 @@ private const val KEY_STATE = "state"
/**
* ViewModel responsible for handling user interactions in the attachments screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class AttachmentsViewModel @Inject constructor(
private val vaultRepo: VaultRepository,
@@ -36,6 +40,7 @@ class AttachmentsViewModel @Inject constructor(
?: AttachmentsState(
cipherId = AttachmentsArgs(savedStateHandle).cipherId,
viewState = AttachmentsState.ViewState.Loading,
dialogState = null,
),
) {
init {
@@ -50,6 +55,7 @@ class AttachmentsViewModel @Inject constructor(
when (action) {
AttachmentsAction.BackClick -> handleBackClick()
AttachmentsAction.SaveClick -> handleSaveClick()
AttachmentsAction.DismissDialogClick -> handleDismissDialogClick()
AttachmentsAction.ChooseFileClick -> handleChooseFileClick()
is AttachmentsAction.FileChoose -> handleFileChoose(action)
is AttachmentsAction.DeleteClick -> handleDeleteClick(action)
@@ -66,6 +72,10 @@ class AttachmentsViewModel @Inject constructor(
// TODO: Handle saving the attachments (BIT-522)
}
private fun handleDismissDialogClick() {
mutableStateFlow.update { it.copy(dialogState = null) }
}
private fun handleChooseFileClick() {
sendEvent(AttachmentsEvent.ShowChooserSheet)
}
@@ -76,13 +86,30 @@ class AttachmentsViewModel @Inject constructor(
}
private fun handleDeleteClick(action: AttachmentsAction.DeleteClick) {
sendEvent(AttachmentsEvent.ShowToast("Not Yet Implemented".asText()))
// TODO: Handle choosing a file the attachments (BIT-522)
onContent { content ->
val cipherView = content.originalCipher ?: return@onContent
mutableStateFlow.update {
it.copy(
dialogState = AttachmentsState.DialogState.Loading(
message = R.string.deleting.asText(),
),
)
}
viewModelScope.launch {
val result = vaultRepo.deleteCipherAttachment(
cipherId = state.cipherId,
attachmentId = action.attachmentId,
cipherView = cipherView,
)
sendAction(AttachmentsAction.Internal.DeleteResultReceive(result))
}
}
}
private fun handleInternalAction(action: AttachmentsAction.Internal) {
when (action) {
is AttachmentsAction.Internal.CipherReceive -> handleCipherReceive(action)
is AttachmentsAction.Internal.DeleteResultReceive -> handleDeleteResultReceive(action)
}
}
@@ -142,6 +169,32 @@ class AttachmentsViewModel @Inject constructor(
}
}
}
private fun handleDeleteResultReceive(action: AttachmentsAction.Internal.DeleteResultReceive) {
when (action.result) {
DeleteAttachmentResult.Error -> {
mutableStateFlow.update {
it.copy(
dialogState = AttachmentsState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
),
)
}
}
DeleteAttachmentResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(AttachmentsEvent.ShowToast(R.string.attachment_deleted.asText()))
}
}
}
private inline fun onContent(
crossinline block: (AttachmentsState.ViewState.Content) -> Unit,
) {
(state.viewState as? AttachmentsState.ViewState.Content)?.let(block)
}
}
/**
@@ -151,6 +204,7 @@ class AttachmentsViewModel @Inject constructor(
data class AttachmentsState(
val cipherId: String,
val viewState: ViewState,
val dialogState: DialogState?,
) : Parcelable {
/**
* Represents the specific view states for the [AttachmentsScreen].
@@ -174,6 +228,8 @@ data class AttachmentsState(
*/
@Parcelize
data class Content(
@IgnoredOnParcel
val originalCipher: CipherView? = null,
val attachments: List<AttachmentItem>,
) : ViewState()
}
@@ -187,6 +243,28 @@ data class AttachmentsState(
val title: String,
val displaySize: String,
) : Parcelable
/**
* Represents the current state of any dialogs on the screen.
*/
sealed class DialogState : Parcelable {
/**
* Represents a dismissible dialog with the given error [message].
*/
@Parcelize
data class Error(
val title: Text?,
val message: Text,
) : DialogState()
/**
* Represents a loading dialog with the given [message].
*/
@Parcelize
data class Loading(
val message: Text,
) : DialogState()
}
}
/**
@@ -225,6 +303,11 @@ sealed class AttachmentsAction {
*/
data object SaveClick : AttachmentsAction()
/**
* User clicked to dismiss a dialog.
*/
data object DismissDialogClick : AttachmentsAction()
/**
* User clicked to select a new attachment file.
*/
@@ -254,5 +337,12 @@ sealed class AttachmentsAction {
data class CipherReceive(
val cipherDataState: DataState<CipherView?>,
) : Internal()
/**
* The result of deleting the attachment.
*/
data class DeleteResultReceive(
val result: DeleteAttachmentResult,
) : Internal()
}
}

View File

@@ -13,6 +13,7 @@ data class AttachmentsHandlers(
val onChooseFileClick: () -> Unit,
val onFileChoose: (IntentManager.FileData) -> Unit,
val onDeleteClick: (attachmentId: String) -> Unit,
val onDismissRequest: () -> Unit,
) {
companion object {
/**
@@ -30,6 +31,9 @@ data class AttachmentsHandlers(
onDeleteClick = {
viewModel.trySendAction(AttachmentsAction.DeleteClick(it))
},
onDismissRequest = {
viewModel.trySendAction(AttachmentsAction.DismissDialogClick)
},
)
}
}

View File

@@ -8,6 +8,7 @@ import com.x8bit.bitwarden.ui.vault.feature.attachments.AttachmentsState
*/
fun CipherView.toViewState(): AttachmentsState.ViewState.Content =
AttachmentsState.ViewState.Content(
originalCipher = this,
attachments = this
.attachments
.orEmpty()