mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-481: Implement primary Send UI and sharing (#510)
This commit is contained in:
parent
4a39f126dd
commit
e0231f511f
5 changed files with 126 additions and 17 deletions
|
@ -52,4 +52,15 @@ class IntentHandler(private val context: Context) {
|
|||
}
|
||||
startActivity(Intent(Intent.ACTION_VIEW, newUri))
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the share sheet with the given [text].
|
||||
*/
|
||||
fun shareText(text: String) {
|
||||
val sendIntent: Intent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
type = "text/plain"
|
||||
}
|
||||
startActivity(Intent.createChooser(sendIntent, null))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,10 +29,12 @@ import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
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.BitwardenMediumTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
@ -58,6 +60,10 @@ fun SendScreen(
|
|||
intentHandler.launchUri("https://bitwarden.com/products/send".toUri())
|
||||
}
|
||||
|
||||
is SendEvent.ShowShareSheet -> {
|
||||
intentHandler.shareText(event.url)
|
||||
}
|
||||
|
||||
is SendEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
|
||||
|
@ -66,6 +72,10 @@ fun SendScreen(
|
|||
}
|
||||
}
|
||||
|
||||
SendDialogs(
|
||||
dialogState = state.dialogState,
|
||||
)
|
||||
|
||||
val sendHandlers = remember(viewModel) { SendHandlers.create(viewModel) }
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||
state = rememberTopAppBarState(),
|
||||
|
@ -159,3 +169,16 @@ fun SendScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendDialogs(
|
||||
dialogState: SendState.DialogState?,
|
||||
) {
|
||||
when (dialogState) {
|
||||
is SendState.DialogState.Loading -> BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(dialogState.message),
|
||||
)
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ class SendViewModel @Inject constructor(
|
|||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: SendState(
|
||||
viewState = SendState.ViewState.Loading,
|
||||
dialogState = null,
|
||||
),
|
||||
) {
|
||||
|
||||
|
@ -81,6 +82,7 @@ class SendViewModel @Inject constructor(
|
|||
viewState = SendState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
dialogState = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +96,7 @@ class SendViewModel @Inject constructor(
|
|||
.environmentUrlData
|
||||
.baseWebSendUrl,
|
||||
),
|
||||
dialogState = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +115,7 @@ class SendViewModel @Inject constructor(
|
|||
.asText()
|
||||
.concat(R.string.internet_connection_required_message.asText()),
|
||||
),
|
||||
dialogState = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +158,9 @@ class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleSyncClick() {
|
||||
// TODO: Add loading dialog state BIT-481
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = SendState.DialogState.Loading(R.string.syncing.asText()))
|
||||
}
|
||||
vaultRepo.sync()
|
||||
}
|
||||
|
||||
|
@ -163,22 +169,21 @@ class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleSendClick(action: SendAction.SendClick) {
|
||||
// TODO: Navigate to the edit send screen BIT-??
|
||||
// TODO: Navigate to the edit send screen (BIT-1387)
|
||||
sendEvent(SendEvent.ShowToast("Not yet implemented".asText()))
|
||||
}
|
||||
|
||||
private fun handleShareClick(action: SendAction.ShareClick) {
|
||||
// TODO: Create a link and use the share sheet BIT-??
|
||||
sendEvent(SendEvent.ShowToast("Not yet implemented".asText()))
|
||||
sendEvent(SendEvent.ShowShareSheet(action.sendItem.shareUrl))
|
||||
}
|
||||
|
||||
private fun handleFileTypeClick() {
|
||||
// TODO: Navigate to the file type send list screen BIT-??
|
||||
// TODO: Navigate to the file type send list screen (BIT-1388)
|
||||
sendEvent(SendEvent.ShowToast("Not yet implemented".asText()))
|
||||
}
|
||||
|
||||
private fun handleTextTypeClick() {
|
||||
// TODO: Navigate to the text type send list screen BIT-??
|
||||
// TODO: Navigate to the text type send list screen (BIT-1388)
|
||||
sendEvent(SendEvent.ShowToast("Not yet implemented".asText()))
|
||||
}
|
||||
}
|
||||
|
@ -189,6 +194,7 @@ class SendViewModel @Inject constructor(
|
|||
@Parcelize
|
||||
data class SendState(
|
||||
val viewState: ViewState,
|
||||
val dialogState: DialogState?,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
|
@ -259,6 +265,20 @@ data class SendState(
|
|||
override val shouldDisplayFab: Boolean get() = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
*/
|
||||
sealed class DialogState : Parcelable {
|
||||
|
||||
/**
|
||||
* Represents a loading dialog with the given [message].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Loading(
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -353,6 +373,11 @@ sealed class SendEvent {
|
|||
*/
|
||||
data object NavigateToAboutSend : SendEvent()
|
||||
|
||||
/**
|
||||
* Show a share sheet with the given content.
|
||||
*/
|
||||
data class ShowShareSheet(val url: String) : SendEvent()
|
||||
|
||||
/**
|
||||
* Show a toast to the user.
|
||||
*/
|
||||
|
|
|
@ -41,6 +41,7 @@ class SendScreenTest : BaseComposeTest() {
|
|||
|
||||
private val intentHandler = mockk<IntentHandler> {
|
||||
every { launchUri(any()) } just runs
|
||||
every { shareText(any()) } just runs
|
||||
}
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<SendEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
|
@ -74,6 +75,15 @@ class SendScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on ShowShareSheet should call shareText on IntentHandler`() {
|
||||
val text = "sharable stuff"
|
||||
mutableEventFlow.tryEmit(SendEvent.ShowShareSheet(text))
|
||||
verify {
|
||||
intentHandler.shareText(text)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on overflow item click should display menu`() {
|
||||
composeTestRule
|
||||
|
@ -474,10 +484,27 @@ class SendScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loading dialog should be displayed according to state`() {
|
||||
val loadingMessage = "syncing"
|
||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText(loadingMessage).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = SendState.DialogState.Loading(loadingMessage.asText()))
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(loadingMessage)
|
||||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE: SendState = SendState(
|
||||
viewState = SendState.ViewState.Loading,
|
||||
dialogState = null,
|
||||
)
|
||||
|
||||
private val DEFAULT_SEND_ITEM: SendState.ViewState.Content.SendItem =
|
||||
|
|
|
@ -114,6 +114,12 @@ class SendViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.trySendAction(SendAction.SyncClick)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = SendState.DialogState.Loading(R.string.syncing.asText()),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify {
|
||||
vaultRepo.sync()
|
||||
}
|
||||
|
@ -146,12 +152,15 @@ class SendViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `ShareClick should emit ShowToast`() = runTest {
|
||||
fun `ShareClick should emit ShowShareSheet`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val sendItem = mockk<SendState.ViewState.Content.SendItem>()
|
||||
val testUrl = "www.test.com"
|
||||
val sendItem = mockk<SendState.ViewState.Content.SendItem> {
|
||||
every { shareUrl } returns testUrl
|
||||
}
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SendAction.ShareClick(sendItem))
|
||||
assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem())
|
||||
assertEquals(SendEvent.ShowShareSheet(testUrl), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,7 +184,8 @@ class SendViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `VaultRepository SendData Error should update view state to Error`() {
|
||||
val viewModel = createViewModel()
|
||||
val dialogState = SendState.DialogState.Loading(R.string.syncing.asText())
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState))
|
||||
|
||||
mutableSendDataFlow.value = DataState.Error(Throwable("Fail"))
|
||||
|
||||
|
@ -184,6 +194,7 @@ class SendViewModelTest : BaseViewModelTest() {
|
|||
viewState = SendState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
dialogState = null,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -191,7 +202,8 @@ class SendViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `VaultRepository SendData Loaded should update view state`() {
|
||||
val viewModel = createViewModel()
|
||||
val dialogState = SendState.DialogState.Loading(R.string.syncing.asText())
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState))
|
||||
val viewState = mockk<SendState.ViewState.Content>()
|
||||
val sendData = mockk<SendData> {
|
||||
every {
|
||||
|
@ -201,24 +213,29 @@ class SendViewModelTest : BaseViewModelTest() {
|
|||
|
||||
mutableSendDataFlow.value = DataState.Loaded(sendData)
|
||||
|
||||
assertEquals(SendState(viewState = viewState), viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
SendState(viewState = viewState, dialogState = null),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VaultRepository SendData Loading should update view state to Loading`() {
|
||||
val viewModel = createViewModel()
|
||||
val dialogState = SendState.DialogState.Loading(R.string.syncing.asText())
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState))
|
||||
|
||||
mutableSendDataFlow.value = DataState.Loading
|
||||
|
||||
assertEquals(
|
||||
SendState(viewState = SendState.ViewState.Loading),
|
||||
SendState(viewState = SendState.ViewState.Loading, dialogState = dialogState),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VaultRepository SendData NoNetwork should update view state to Error`() {
|
||||
val viewModel = createViewModel()
|
||||
val dialogState = SendState.DialogState.Loading(R.string.syncing.asText())
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState))
|
||||
|
||||
mutableSendDataFlow.value = DataState.NoNetwork()
|
||||
|
||||
|
@ -229,6 +246,7 @@ class SendViewModelTest : BaseViewModelTest() {
|
|||
.asText()
|
||||
.concat(R.string.internet_connection_required_message.asText()),
|
||||
),
|
||||
dialogState = null,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -236,7 +254,8 @@ class SendViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `VaultRepository SendData Pending should update view state`() {
|
||||
val viewModel = createViewModel()
|
||||
val dialogState = SendState.DialogState.Loading(R.string.syncing.asText())
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE.copy(dialogState = dialogState))
|
||||
val viewState = mockk<SendState.ViewState.Content>()
|
||||
val sendData = mockk<SendData> {
|
||||
every {
|
||||
|
@ -246,7 +265,10 @@ class SendViewModelTest : BaseViewModelTest() {
|
|||
|
||||
mutableSendDataFlow.value = DataState.Pending(sendData)
|
||||
|
||||
assertEquals(SendState(viewState = viewState), viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
SendState(viewState = viewState, dialogState = dialogState),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
|
@ -269,4 +291,5 @@ private const val SEND_DATA_EXTENSIONS_PATH: String =
|
|||
|
||||
private val DEFAULT_STATE: SendState = SendState(
|
||||
viewState = SendState.ViewState.Loading,
|
||||
dialogState = null,
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue