Refactor AddSendViewModel to support loading and error states. (#524)

This commit is contained in:
David Perez
2024-01-07 20:05:33 -06:00
committed by Álison Fernandes
parent 1e8d603b61
commit 978e72899b
6 changed files with 635 additions and 367 deletions

View File

@@ -0,0 +1,281 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.BitwardenSegmentedButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState
import com.x8bit.bitwarden.ui.tools.feature.send.SendDeletionDateChooser
import com.x8bit.bitwarden.ui.tools.feature.send.SendExpirationDateChooser
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers
/**
* Content view for the [AddSendScreen].
*/
@Suppress("LongMethod")
@Composable
fun AddSendContent(
state: AddSendState.ViewState.Content,
addSendHandlers: AddSendHandlers,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.verticalScroll(rememberScrollState()),
) {
BitwardenTextField(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.name),
hint = stringResource(id = R.string.name_info),
value = state.common.name,
onValueChange = addSendHandlers.onNamChange,
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.type),
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenSegmentedButton(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
options = listOf(
SegmentedButtonState(
text = stringResource(id = R.string.file),
onClick = addSendHandlers.onFileTypeSelect,
isChecked = state.selectedType is AddSendState.ViewState.Content.SendType.File,
),
SegmentedButtonState(
text = stringResource(id = R.string.text),
onClick = addSendHandlers.onTextTypeSelect,
isChecked = state.selectedType is AddSendState.ViewState.Content.SendType.Text,
),
),
)
Spacer(modifier = Modifier.height(16.dp))
when (val type = state.selectedType) {
is AddSendState.ViewState.Content.SendType.File -> {
BitwardenListHeaderText(
label = stringResource(id = R.string.file),
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = stringResource(id = R.string.no_file_chosen),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenFilledTonalButton(
label = stringResource(id = R.string.choose_file),
onClick = addSendHandlers.onChooseFileCLick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.max_file_size),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 32.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.type_file_info),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
is AddSendState.ViewState.Content.SendType.Text -> {
BitwardenTextField(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.text),
hint = stringResource(id = R.string.type_text_info),
value = type.input,
onValueChange = addSendHandlers.onTextChange,
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenWideSwitch(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.hide_text_by_default),
isChecked = type.isHideByDefaultChecked,
onCheckedChange = addSendHandlers.onIsHideByDefaultToggle,
)
}
}
Spacer(modifier = Modifier.height(16.dp))
AddSendOptions(
state = state,
onMaxAccessCountChange = addSendHandlers.onMaxAccessCountChange,
onPasswordChange = addSendHandlers.onPasswordChange,
onNoteChange = addSendHandlers.onNoteChange,
onHideEmailChecked = addSendHandlers.onHideEmailToggle,
onDeactivateSendChecked = addSendHandlers.onDeactivateSendToggle,
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
/**
* Displays a collapsable set of new send options.
*
* @param state The content state.
* @param onMaxAccessCountChange called when max access count changes.
* @param onPasswordChange called when the password changes.
* @param onNoteChange called when the notes changes.
* @param onHideEmailChecked called when hide email is checked.
* @param onDeactivateSendChecked called when deactivate send is checked.
*/
@Suppress("LongMethod")
@Composable
private fun AddSendOptions(
state: AddSendState.ViewState.Content,
onMaxAccessCountChange: (Int) -> Unit,
onPasswordChange: (String) -> Unit,
onNoteChange: (String) -> Unit,
onHideEmailChecked: (Boolean) -> Unit,
onDeactivateSendChecked: (Boolean) -> Unit,
) {
var isExpanded by rememberSaveable { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
onClickLabel = if (isExpanded) {
stringResource(id = R.string.options_expanded)
} else {
stringResource(id = R.string.options_collapsed)
},
onClick = { isExpanded = !isExpanded },
)
.padding(16.dp)
.semantics(mergeDescendants = true) {},
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(id = R.string.options),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(end = 8.dp),
)
Icon(
painter = if (isExpanded) {
painterResource(R.drawable.ic_expand_up)
} else {
painterResource(R.drawable.ic_expand_down)
},
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
}
// Hide all content if not expanded:
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically(),
modifier = Modifier.clipToBounds(),
) {
Column {
SendDeletionDateChooser(
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
SendExpirationDateChooser(
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenStepper(
label = stringResource(id = R.string.maximum_access_count),
value = state.common.maxAccessCount,
onValueChange = onMaxAccessCountChange,
isDecrementEnabled = state.common.maxAccessCount != null,
modifier = Modifier
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.maximum_access_count_info),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.new_password),
hint = stringResource(id = R.string.password_info),
value = state.common.passwordInput,
onValueChange = onPasswordChange,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.notes),
hint = stringResource(id = R.string.notes_info),
value = state.common.noteInput,
onValueChange = onNoteChange,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenWideSwitch(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.hide_email),
isChecked = state.common.isHideEmailChecked,
onCheckedChange = onHideEmailChecked,
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenWideSwitch(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.disable_send),
isChecked = state.common.isDeactivateChecked,
onCheckedChange = onDeactivateSendChecked,
)
}
}
}

View File

@@ -1,61 +1,30 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
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.BitwardenFilledTonalButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenSegmentedButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState
import com.x8bit.bitwarden.ui.tools.feature.send.SendDeletionDateChooser
import com.x8bit.bitwarden.ui.tools.feature.send.SendExpirationDateChooser
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers
/**
* Displays new send UX.
@@ -104,249 +73,25 @@ fun AddSendScreen(
)
},
) { innerPadding ->
Column(
modifier = Modifier
.imePadding()
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(paddingValues = innerPadding),
) {
BitwardenTextField(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.name),
hint = stringResource(id = R.string.name_info),
value = state.name,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.NameChange(it)) }
},
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.type),
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenSegmentedButton(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
options = listOf(
SegmentedButtonState(
text = stringResource(id = R.string.file),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.FileTypeClick) }
},
isChecked = state.selectedType is AddSendState.SendType.File,
),
SegmentedButtonState(
text = stringResource(id = R.string.text),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.TextTypeClick) }
},
isChecked = state.selectedType is AddSendState.SendType.Text,
),
),
)
Spacer(modifier = Modifier.height(16.dp))
when (val type = state.selectedType) {
is AddSendState.SendType.File -> {
BitwardenListHeaderText(
label = stringResource(id = R.string.file),
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = stringResource(id = R.string.no_file_chosen),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenFilledTonalButton(
label = stringResource(id = R.string.choose_file),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.ChooseFileClick) }
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.max_file_size),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 32.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.type_file_info),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
val modifier = Modifier
.imePadding()
.fillMaxSize()
.padding(paddingValues = innerPadding)
is AddSendState.SendType.Text -> {
BitwardenTextField(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.text),
hint = stringResource(id = R.string.type_text_info),
value = type.input,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.TextChange(it)) }
},
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenWideSwitch(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.hide_text_by_default),
isChecked = type.isHideByDefaultChecked,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.HideByDefaultToggle(it)) }
},
)
}
}
Spacer(modifier = Modifier.height(16.dp))
NewSendOptions(
state = state,
onMaxAccessCountChange = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.MaxAccessCountChange(it)) }
},
onPasswordChange = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.PasswordChange(it)) }
},
onNoteChange = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.NoteChange(it)) }
},
onHideEmailChecked = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.HideMyEmailToggle(it)) }
},
onDeactivateSendChecked = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.DeactivateThisSendToggle(it)) }
},
when (val viewState = state.viewState) {
is AddSendState.ViewState.Content -> AddSendContent(
state = viewState,
addSendHandlers = remember(viewModel) { AddSendHandlers.create(viewModel) },
modifier = modifier,
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
/**
* Displays a collapsable set of new send options.
*
* @param state state.
* @param onMaxAccessCountChange called when max access count changes.
* @param onPasswordChange called when the password changes.
* @param onNoteChange called when the notes changes.
* @param onHideEmailChecked called when hide email is checked.
* @param onDeactivateSendChecked called when deactivate send is checked.
*/
@Suppress("LongMethod")
@Composable
private fun NewSendOptions(
state: AddSendState,
onMaxAccessCountChange: (Int) -> Unit,
onPasswordChange: (String) -> Unit,
onNoteChange: (String) -> Unit,
onHideEmailChecked: (Boolean) -> Unit,
onDeactivateSendChecked: (Boolean) -> Unit,
) {
var isExpanded by rememberSaveable { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
onClickLabel = if (isExpanded) {
stringResource(id = R.string.options_expanded)
} else {
stringResource(id = R.string.options_collapsed)
},
onClick = { isExpanded = !isExpanded },
is AddSendState.ViewState.Error -> BitwardenErrorContent(
message = viewState.message(),
modifier = modifier,
)
.padding(16.dp)
.semantics(mergeDescendants = true) {},
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(id = R.string.options),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(end = 8.dp),
)
Icon(
painter = if (isExpanded) {
painterResource(R.drawable.ic_expand_up)
} else {
painterResource(R.drawable.ic_expand_down)
},
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
}
// Hide all content if not expanded:
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically(),
modifier = Modifier.clipToBounds(),
) {
Column {
SendDeletionDateChooser(
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
SendExpirationDateChooser(
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenStepper(
label = stringResource(id = R.string.maximum_access_count),
value = state.maxAccessCount,
onValueChange = onMaxAccessCountChange,
isDecrementEnabled = state.maxAccessCount != null,
modifier = Modifier
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.maximum_access_count_info),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.new_password),
hint = stringResource(id = R.string.password_info),
value = state.passwordInput,
onValueChange = onPasswordChange,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.notes),
hint = stringResource(id = R.string.notes_info),
value = state.noteInput,
onValueChange = onNoteChange,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenWideSwitch(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.hide_email),
isChecked = state.isHideEmailChecked,
onCheckedChange = onHideEmailChecked,
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenWideSwitch(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.disable_send),
isChecked = state.isDeactivateChecked,
onCheckedChange = onDeactivateSendChecked,
AddSendState.ViewState.Loading -> BitwardenLoadingContent(
modifier = modifier,
)
}
}

View File

@@ -4,6 +4,7 @@ import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -22,15 +23,19 @@ class AddSendViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<AddSendState, AddSendEvent, AddSendAction>(
initialState = savedStateHandle[KEY_STATE] ?: AddSendState(
name = "",
maxAccessCount = null,
passwordInput = "",
noteInput = "",
isHideEmailChecked = false,
isDeactivateChecked = false,
selectedType = AddSendState.SendType.Text(
input = "",
isHideByDefaultChecked = false,
viewState = AddSendState.ViewState.Content(
common = AddSendState.ViewState.Content.Common(
name = "",
maxAccessCount = null,
passwordInput = "",
noteInput = "",
isHideEmailChecked = false,
isDeactivateChecked = false,
),
selectedType = AddSendState.ViewState.Content.SendType.Text(
input = "",
isHideByDefaultChecked = false,
),
),
),
) {
@@ -58,25 +63,25 @@ class AddSendViewModel @Inject constructor(
}
private fun handlePasswordChange(action: AddSendAction.PasswordChange) {
mutableStateFlow.update {
updateCommonContent {
it.copy(passwordInput = action.input)
}
}
private fun handleNoteChange(action: AddSendAction.NoteChange) {
mutableStateFlow.update {
updateCommonContent {
it.copy(noteInput = action.input)
}
}
private fun handleHideMyEmailToggle(action: AddSendAction.HideMyEmailToggle) {
mutableStateFlow.update {
updateCommonContent {
it.copy(isHideEmailChecked = action.isChecked)
}
}
private fun handleDeactivateThisSendToggle(action: AddSendAction.DeactivateThisSendToggle) {
mutableStateFlow.update {
updateCommonContent {
it.copy(isDeactivateChecked = action.isChecked)
}
}
@@ -86,36 +91,37 @@ class AddSendViewModel @Inject constructor(
private fun handleSaveClick() = sendEvent(AddSendEvent.ShowToast("Save Not Implemented"))
private fun handleNameChange(action: AddSendAction.NameChange) {
mutableStateFlow.update {
updateCommonContent {
it.copy(name = action.input)
}
}
private fun handleFileTypeClick() {
mutableStateFlow.update {
it.copy(selectedType = AddSendState.SendType.File)
updateContent {
it.copy(selectedType = AddSendState.ViewState.Content.SendType.File)
}
}
private fun handleTextTypeClick() {
mutableStateFlow.update {
it.copy(selectedType = AddSendState.SendType.Text("", isHideByDefaultChecked = false))
updateContent {
it.copy(
selectedType = AddSendState.ViewState.Content.SendType.Text(
input = "",
isHideByDefaultChecked = false,
),
)
}
}
private fun handleTextChange(action: AddSendAction.TextChange) {
val currentSendInput =
mutableStateFlow.value.selectedType as? AddSendState.SendType.Text ?: return
mutableStateFlow.update {
it.copy(selectedType = currentSendInput.copy(input = action.input))
updateTextContent {
it.copy(input = action.input)
}
}
private fun handleHideByDefaultToggle(action: AddSendAction.HideByDefaultToggle) {
val currentSendInput =
mutableStateFlow.value.selectedType as? AddSendState.SendType.Text ?: return
mutableStateFlow.update {
it.copy(selectedType = currentSendInput.copy(isHideByDefaultChecked = action.isChecked))
updateTextContent {
it.copy(isHideByDefaultChecked = action.isChecked)
}
}
@@ -125,8 +131,54 @@ class AddSendViewModel @Inject constructor(
}
private fun handleMaxAccessCountChange(action: AddSendAction.MaxAccessCountChange) {
mutableStateFlow.update {
it.copy(maxAccessCount = action.value)
updateCommonContent { it.copy(maxAccessCount = action.value) }
}
private inline fun onContent(
crossinline block: (AddSendState.ViewState.Content) -> Unit,
) {
(state.viewState as? AddSendState.ViewState.Content)?.let(block)
}
private inline fun updateContent(
crossinline block: (
AddSendState.ViewState.Content,
) -> AddSendState.ViewState.Content?,
) {
val currentViewState = state.viewState
val updatedContent = (currentViewState as? AddSendState.ViewState.Content)
?.let(block)
?: return
mutableStateFlow.update { it.copy(viewState = updatedContent) }
}
private inline fun updateCommonContent(
crossinline block: (
AddSendState.ViewState.Content.Common,
) -> AddSendState.ViewState.Content.Common,
) {
updateContent { it.copy(common = block(it.common)) }
}
private inline fun updateFileContent(
crossinline block: (
AddSendState.ViewState.Content.SendType.File,
) -> AddSendState.ViewState.Content.SendType.File,
) {
updateContent { currentContent ->
(currentContent.selectedType as? AddSendState.ViewState.Content.SendType.File)
?.let { currentContent.copy(selectedType = block(it)) }
}
}
private inline fun updateTextContent(
crossinline block: (
AddSendState.ViewState.Content.SendType.Text,
) -> AddSendState.ViewState.Content.SendType.Text,
) {
updateContent { currentContent ->
(currentContent.selectedType as? AddSendState.ViewState.Content.SendType.Text)
?.let { currentContent.copy(selectedType = block(it)) }
}
}
}
@@ -136,34 +188,68 @@ class AddSendViewModel @Inject constructor(
*/
@Parcelize
data class AddSendState(
val name: String,
val selectedType: SendType,
// Null here means "not set"
val maxAccessCount: Int?,
val passwordInput: String,
val noteInput: String,
val isHideEmailChecked: Boolean,
val isDeactivateChecked: Boolean,
val viewState: ViewState,
) : Parcelable {
/**
* Models what type the user is trying to send.
* Represents the specific view states for the [AddSendScreen].
*/
sealed class SendType : Parcelable {
sealed class ViewState : Parcelable {
/**
* Sending a file.
* Represents an error state for the [AddSendScreen].
*/
@Parcelize
data object File : SendType()
data class Error(val message: Text) : ViewState()
/**
* Sending text.
* Loading state for the [AddSendScreen], signifying that the content is being processed.
*/
@Parcelize
data class Text(
val input: String,
val isHideByDefaultChecked: Boolean,
) : SendType()
data object Loading : ViewState()
/**
* Represents a loaded content state for the [AddSendScreen].
*/
@Parcelize
data class Content(
val common: Common,
val selectedType: SendType,
) : ViewState() {
/**
* Content data that is common for all item types.
*/
@Parcelize
data class Common(
val name: String,
// Null here means "not set"
val maxAccessCount: Int?,
val passwordInput: String,
val noteInput: String,
val isHideEmailChecked: Boolean,
val isDeactivateChecked: Boolean,
) : Parcelable
/**
* Models what type the user is trying to send.
*/
sealed class SendType : Parcelable {
/**
* Sending a file.
*/
@Parcelize
data object File : SendType()
/**
* Sending text.
*/
@Parcelize
data class Text(
val input: String,
val isHideByDefaultChecked: Boolean,
) : SendType()
}
}
}
}

View File

@@ -0,0 +1,53 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendAction
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendViewModel
/**
* A collection of handler functions for managing actions within the context of adding
* send items.
*/
data class AddSendHandlers(
val onNamChange: (String) -> Unit,
val onFileTypeSelect: () -> Unit,
val onTextTypeSelect: () -> Unit,
val onChooseFileCLick: () -> Unit,
val onTextChange: (String) -> Unit,
val onIsHideByDefaultToggle: (Boolean) -> Unit,
val onMaxAccessCountChange: (Int) -> Unit,
val onPasswordChange: (String) -> Unit,
val onNoteChange: (String) -> Unit,
val onHideEmailToggle: (Boolean) -> Unit,
val onDeactivateSendToggle: (Boolean) -> Unit,
) {
companion object {
/**
* Creates an instance of [AddSendHandlers] by binding actions to the provided
* [AddSendViewModel].
*/
fun create(
viewModel: AddSendViewModel,
): AddSendHandlers =
AddSendHandlers(
onNamChange = { viewModel.trySendAction(AddSendAction.NameChange(it)) },
onFileTypeSelect = { viewModel.trySendAction(AddSendAction.FileTypeClick) },
onTextTypeSelect = { viewModel.trySendAction(AddSendAction.TextTypeClick) },
onChooseFileCLick = { viewModel.trySendAction(AddSendAction.ChooseFileClick) },
onTextChange = { viewModel.trySendAction(AddSendAction.TextChange(it)) },
onIsHideByDefaultToggle = {
viewModel.trySendAction(AddSendAction.HideByDefaultToggle(it))
},
onMaxAccessCountChange = {
viewModel.trySendAction(AddSendAction.MaxAccessCountChange(it))
},
onPasswordChange = { viewModel.trySendAction(AddSendAction.PasswordChange(it)) },
onNoteChange = { viewModel.trySendAction(AddSendAction.NoteChange(it)) },
onHideEmailToggle = {
viewModel.trySendAction(AddSendAction.HideMyEmailToggle(it))
},
onDeactivateSendToggle = {
viewModel.trySendAction(AddSendAction.DeactivateThisSendToggle(it))
},
)
}
}