mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
Refactor AddSendViewModel to support loading and error states. (#524)
This commit is contained in:
parent
1e8d603b61
commit
978e72899b
6 changed files with 635 additions and 367 deletions
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
|
@ -13,6 +14,8 @@ import androidx.compose.ui.test.performScrollTo
|
|||
import androidx.compose.ui.test.performTextInput
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.util.isProgressBar
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
|
@ -83,7 +86,11 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
)
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(name = "input")
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(name = "input"),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Name")
|
||||
|
@ -113,7 +120,9 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
@Test
|
||||
fun `Choose file button click should send ChooseFileClick`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE.copy(
|
||||
selectedType = AddSendState.SendType.File,
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
selectedType = AddSendState.ViewState.Content.SendType.File,
|
||||
),
|
||||
)
|
||||
composeTestRule
|
||||
.onNodeWithText("Choose file")
|
||||
|
@ -143,9 +152,11 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
selectedType = AddSendState.SendType.Text(
|
||||
input = "input",
|
||||
isHideByDefaultChecked = false,
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
selectedType = AddSendState.ViewState.Content.SendType.Text(
|
||||
input = "input",
|
||||
isHideByDefaultChecked = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -175,9 +186,11 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
selectedType = AddSendState.SendType.Text(
|
||||
input = "",
|
||||
isHideByDefaultChecked = true,
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
selectedType = AddSendState.ViewState.Content.SendType.Text(
|
||||
input = "",
|
||||
isHideByDefaultChecked = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -253,7 +266,11 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
@Test
|
||||
fun `max access count decrement should send MaxAccessCountChange`() = runTest {
|
||||
mutableStateFlow.update {
|
||||
it.copy(maxAccessCount = 3)
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(maxAccessCount = 3),
|
||||
),
|
||||
)
|
||||
}
|
||||
// Expand options section:
|
||||
composeTestRule
|
||||
|
@ -272,7 +289,11 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
fun `max access count decrement when set to 1 should do nothing`() =
|
||||
runTest {
|
||||
mutableStateFlow.update {
|
||||
it.copy(maxAccessCount = 1)
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(maxAccessCount = 1),
|
||||
),
|
||||
)
|
||||
}
|
||||
// Expand options section:
|
||||
composeTestRule
|
||||
|
@ -333,7 +354,9 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
passwordInput = "input",
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(passwordInput = "input"),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
|
@ -376,7 +399,9 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
noteInput = "input",
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(noteInput = "input"),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
|
@ -416,7 +441,9 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isHideEmailChecked = true,
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(isHideEmailChecked = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
|
@ -452,7 +479,9 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isDeactivateChecked = true,
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(isDeactivateChecked = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
|
@ -460,18 +489,60 @@ class AddSendScreenTest : BaseComposeTest() {
|
|||
.assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `progressbar should be displayed according to state`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = AddSendState.ViewState.Loading)
|
||||
}
|
||||
composeTestRule.onNode(isProgressBar).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = AddSendState.ViewState.Error("Fail".asText()))
|
||||
}
|
||||
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
}
|
||||
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error should be displayed according to state`() {
|
||||
val errorMessage = "Fail"
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = AddSendState.ViewState.Error(errorMessage.asText()))
|
||||
}
|
||||
composeTestRule.onNodeWithText(errorMessage).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = AddSendState.ViewState.Loading)
|
||||
}
|
||||
composeTestRule.onNodeWithText(errorMessage).assertDoesNotExist()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = AddSendState(
|
||||
private val DEFAULT_COMMON_STATE = AddSendState.ViewState.Content.Common(
|
||||
name = "",
|
||||
maxAccessCount = null,
|
||||
passwordInput = "",
|
||||
noteInput = "",
|
||||
isHideEmailChecked = false,
|
||||
isDeactivateChecked = false,
|
||||
selectedType = AddSendState.SendType.Text(
|
||||
input = "",
|
||||
isHideByDefaultChecked = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_SELECTED_TYPE_STATE = AddSendState.ViewState.Content.SendType.Text(
|
||||
input = "",
|
||||
isHideByDefaultChecked = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_VIEW_STATE = AddSendState.ViewState.Content(
|
||||
common = DEFAULT_COMMON_STATE,
|
||||
selectedType = DEFAULT_SELECTED_TYPE_STATE,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE = AddSendState(
|
||||
viewState = DEFAULT_VIEW_STATE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `initial state should read from saved state when present`() {
|
||||
val savedState = mockk<AddSendState>()
|
||||
val viewModel = createViewModel(
|
||||
savedStateHandle = SavedStateHandle(mapOf("state" to savedState)),
|
||||
)
|
||||
val viewModel = createViewModel(savedState)
|
||||
assertEquals(savedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
|
@ -55,13 +53,14 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `FileTypeClick and TextTypeClick should toggle sendType`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
selectedType = AddSendState.ViewState.Content.SendType.File,
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.FileTypeClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(selectedType = AddSendState.SendType.File),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.TextTypeClick)
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
|
@ -70,99 +69,132 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `NameChange should update name input`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(name = "input"),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.NameChange("input"))
|
||||
assertEquals(DEFAULT_STATE.copy(name = "input"), awaitItem())
|
||||
assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MaxAccessCountChange should update maxAccessCount`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(maxAccessCount = 5),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.MaxAccessCountChange(5))
|
||||
assertEquals(DEFAULT_STATE.copy(maxAccessCount = 5), awaitItem())
|
||||
assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TextChange should update text input`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
selectedType = AddSendState.ViewState.Content.SendType.Text(
|
||||
input = "input",
|
||||
isHideByDefaultChecked = false,
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.TextChange("input"))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
selectedType = AddSendState.SendType.Text(
|
||||
input = "input",
|
||||
isHideByDefaultChecked = false,
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NoteChange should update note input`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(noteInput = "input"),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.NoteChange("input"))
|
||||
assertEquals(DEFAULT_STATE.copy(noteInput = "input"), awaitItem())
|
||||
assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PasswordChange should update note input`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(passwordInput = "input"),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.PasswordChange("input"))
|
||||
assertEquals(DEFAULT_STATE.copy(passwordInput = "input"), awaitItem())
|
||||
assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DeactivateThisSendToggle should update isDeactivateChecked`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(isDeactivateChecked = true),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.DeactivateThisSendToggle(true))
|
||||
assertEquals(DEFAULT_STATE.copy(isDeactivateChecked = true), awaitItem())
|
||||
assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HideMyEmailToggle should update isHideEmailChecked`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(isHideEmailChecked = true),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.HideMyEmailToggle(true))
|
||||
assertEquals(DEFAULT_STATE.copy(isHideEmailChecked = true), awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.HideMyEmailToggle(isChecked = true))
|
||||
assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
savedStateHandle: SavedStateHandle = SavedStateHandle(),
|
||||
state: AddSendState? = null,
|
||||
): AddSendViewModel = AddSendViewModel(
|
||||
savedStateHandle = savedStateHandle,
|
||||
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = AddSendState(
|
||||
private val DEFAULT_COMMON_STATE = AddSendState.ViewState.Content.Common(
|
||||
name = "",
|
||||
maxAccessCount = null,
|
||||
passwordInput = "",
|
||||
noteInput = "",
|
||||
isHideEmailChecked = false,
|
||||
isDeactivateChecked = false,
|
||||
selectedType = AddSendState.SendType.Text(
|
||||
input = "",
|
||||
isHideByDefaultChecked = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_SELECTED_TYPE_STATE = AddSendState.ViewState.Content.SendType.Text(
|
||||
input = "",
|
||||
isHideByDefaultChecked = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_VIEW_STATE = AddSendState.ViewState.Content(
|
||||
common = DEFAULT_COMMON_STATE,
|
||||
selectedType = DEFAULT_SELECTED_TYPE_STATE,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE = AddSendState(
|
||||
viewState = DEFAULT_VIEW_STATE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue