mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
Add the overflow menu to the send screen (#469)
This commit is contained in:
parent
cf2e87c9f5
commit
7a8da67944
4 changed files with 223 additions and 28 deletions
|
@ -16,40 +16,58 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.unit.IntSize
|
||||
import androidx.core.net.toUri
|
||||
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.base.util.IntentHandler
|
||||
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.OverflowMenuItemData
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* UI for the send screen.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SendScreen(
|
||||
onNavigateNewSend: () -> Unit,
|
||||
viewModel: SendViewModel = hiltViewModel(),
|
||||
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val scrollBehavior =
|
||||
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
||||
val context = LocalContext.current
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
is SendEvent.NavigateNewSend -> onNavigateNewSend()
|
||||
is SendEvent.ShowToast -> Toast
|
||||
.makeText(context, event.messsage(context.resources), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
|
||||
is SendEvent.NavigateToAboutSend -> {
|
||||
intentHandler.launchUri("https://bitwarden.com/products/send".toUri())
|
||||
}
|
||||
|
||||
is SendEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||
state = rememberTopAppBarState(),
|
||||
)
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenMediumTopAppBar(
|
||||
title = stringResource(id = R.string.send),
|
||||
|
@ -61,6 +79,28 @@ fun SendScreen(
|
|||
{ viewModel.trySendAction(SendAction.SearchClick) }
|
||||
},
|
||||
)
|
||||
BitwardenOverflowActionItem(
|
||||
menuItemDataList = persistentListOf(
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = R.string.sync),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SendAction.SyncClick) }
|
||||
},
|
||||
),
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = R.string.lock),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SendAction.LockClick) }
|
||||
},
|
||||
),
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = R.string.about_send),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SendAction.AboutSendClick) }
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
@ -86,7 +126,7 @@ fun SendScreen(
|
|||
}
|
||||
},
|
||||
) { padding ->
|
||||
Box(Modifier.padding(padding)) {
|
||||
Box(modifier = Modifier.padding(padding)) {
|
||||
when (state) {
|
||||
SendState.Empty -> SendEmpty(
|
||||
remember(viewModel) {
|
||||
|
|
|
@ -2,13 +2,11 @@ package com.x8bit.bitwarden.ui.tools.feature.send
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -20,19 +18,29 @@ private const val KEY_STATE = "state"
|
|||
@HiltViewModel
|
||||
class SendViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val vaultRepo: VaultRepository,
|
||||
) : BaseViewModel<SendState, SendEvent, SendAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: SendState.Empty,
|
||||
) {
|
||||
|
||||
init {
|
||||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
override fun handleAction(action: SendAction): Unit = when (action) {
|
||||
SendAction.AboutSendClick -> handleAboutSendClick()
|
||||
SendAction.AddSendClick -> handleAddSendClick()
|
||||
SendAction.LockClick -> handleLockClick()
|
||||
SendAction.SearchClick -> handleSearchClick()
|
||||
SendAction.SyncClick -> handleSyncClick()
|
||||
}
|
||||
|
||||
override fun handleAction(action: SendAction): Unit = when (action) {
|
||||
SendAction.AddSendClick -> handleSendClick()
|
||||
SendAction.SearchClick -> handleSearchClick()
|
||||
private fun handleAboutSendClick() {
|
||||
sendEvent(SendEvent.NavigateToAboutSend)
|
||||
}
|
||||
|
||||
private fun handleAddSendClick() {
|
||||
sendEvent(SendEvent.NavigateNewSend)
|
||||
}
|
||||
|
||||
private fun handleLockClick() {
|
||||
vaultRepo.lockVaultForCurrentUser()
|
||||
}
|
||||
|
||||
private fun handleSearchClick() {
|
||||
|
@ -40,7 +48,10 @@ class SendViewModel @Inject constructor(
|
|||
sendEvent(SendEvent.ShowToast("Search Not Implemented".asText()))
|
||||
}
|
||||
|
||||
private fun handleSendClick() = sendEvent(SendEvent.NavigateNewSend)
|
||||
private fun handleSyncClick() {
|
||||
// TODO: Add loading dialog state BIT-481
|
||||
vaultRepo.sync()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,15 +69,30 @@ sealed class SendState : Parcelable {
|
|||
* Models actions for the send screen.
|
||||
*/
|
||||
sealed class SendAction {
|
||||
/**
|
||||
* User clicked the about send button.
|
||||
*/
|
||||
data object AboutSendClick : SendAction()
|
||||
|
||||
/**
|
||||
* User clicked add a send.
|
||||
*/
|
||||
data object AddSendClick : SendAction()
|
||||
|
||||
/**
|
||||
* User clicked the lock button.
|
||||
*/
|
||||
data object LockClick : SendAction()
|
||||
|
||||
/**
|
||||
* User clicked search button.
|
||||
*/
|
||||
data object SearchClick : SendAction()
|
||||
|
||||
/**
|
||||
* User clicked the sync button.
|
||||
*/
|
||||
data object SyncClick : SendAction()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,8 +104,13 @@ sealed class SendEvent {
|
|||
*/
|
||||
data object NavigateNewSend : SendEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the about send screen.
|
||||
*/
|
||||
data object NavigateToAboutSend : SendEvent()
|
||||
|
||||
/**
|
||||
* Show a toast to the user.
|
||||
*/
|
||||
data class ShowToast(val messsage: Text) : SendEvent()
|
||||
data class ShowToast(val message: Text) : SendEvent()
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
package com.x8bit.bitwarden.ui.tools.feature.send
|
||||
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDisplayed
|
||||
import androidx.compose.ui.test.isPopup
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
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.IntentHandler
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
@ -15,9 +21,9 @@ import org.junit.Test
|
|||
class SendScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateToNewSendCalled = false
|
||||
private val mutableEventFlow = MutableSharedFlow<SendEvent>(
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
|
||||
private val intentHandler = mockk<IntentHandler>()
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<SendEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val viewModel = mockk<SendViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
|
@ -30,10 +36,79 @@ class SendScreenTest : BaseComposeTest() {
|
|||
SendScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateNewSend = { onNavigateToNewSendCalled = true },
|
||||
intentHandler = intentHandler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on overflow item click should display menu`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(label = "More")
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Sync")
|
||||
.filterToOne(hasAnyAncestor(isPopup()))
|
||||
.isDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Lock")
|
||||
.filterToOne(hasAnyAncestor(isPopup()))
|
||||
.isDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "About Send")
|
||||
.filterToOne(hasAnyAncestor(isPopup()))
|
||||
.isDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on sync click should send SyncClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(label = "More")
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Sync")
|
||||
.filterToOne(hasAnyAncestor(isPopup()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(SendAction.SyncClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on lock click should send LockClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(label = "More")
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Lock")
|
||||
.filterToOne(hasAnyAncestor(isPopup()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(SendAction.LockClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on about send click should send AboutSendClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(label = "More")
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "About Send")
|
||||
.filterToOne(hasAnyAncestor(isPopup()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(SendAction.AboutSendClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on add item FAB click should send AddItemClick`() {
|
||||
composeTestRule
|
||||
|
|
|
@ -2,44 +2,93 @@ package com.x8bit.bitwarden.ui.tools.feature.send
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SendViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val vaultRepo: VaultRepository = mockk()
|
||||
|
||||
@Test
|
||||
fun `initial state should be Empty`() {
|
||||
val viewModel = SendViewModel(SavedStateHandle())
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(SendState.Empty, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should read from saved state when present`() {
|
||||
val savedState = mockk<SendState>()
|
||||
val handle = SavedStateHandle(mapOf("state" to savedState))
|
||||
val viewModel = SendViewModel(handle)
|
||||
val viewModel = createViewModel(state = savedState)
|
||||
assertEquals(savedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AboutSendClick should emit NavigateToAboutSend`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SendAction.AboutSendClick)
|
||||
assertEquals(SendEvent.NavigateToAboutSend, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AddSendClick should emit NavigateNewSend`() = runTest {
|
||||
val viewModel = SendViewModel(SavedStateHandle())
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SendAction.AddSendClick)
|
||||
assertEquals(SendEvent.NavigateNewSend, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LockClick should lock the vault`() {
|
||||
val viewModel = createViewModel()
|
||||
every { vaultRepo.lockVaultForCurrentUser() } just runs
|
||||
|
||||
viewModel.trySendAction(SendAction.LockClick)
|
||||
|
||||
verify {
|
||||
vaultRepo.lockVaultForCurrentUser()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SearchClick should emit ShowToast`() = runTest {
|
||||
val viewModel = SendViewModel(SavedStateHandle())
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SendAction.SearchClick)
|
||||
assertEquals(SendEvent.ShowToast("Search Not Implemented".asText()), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SyncClick should call sync`() {
|
||||
val viewModel = createViewModel()
|
||||
every { vaultRepo.sync() } just runs
|
||||
|
||||
viewModel.trySendAction(SendAction.SyncClick)
|
||||
|
||||
verify {
|
||||
vaultRepo.sync()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
state: SendState? = null,
|
||||
vaultRepository: VaultRepository = vaultRepo,
|
||||
): SendViewModel = SendViewModel(
|
||||
savedStateHandle = SavedStateHandle().apply {
|
||||
set("state", state)
|
||||
},
|
||||
vaultRepo = vaultRepository,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue