diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt index 6d3e8a096..ea6a6eb6b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -33,16 +33,19 @@ import androidx.hilt.navigation.compose.hiltViewModel 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.base.util.asText import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent +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.BitwardenTwoButtonDialog +import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType @@ -87,7 +90,7 @@ fun VaultScreen( VaultEvent.NavigateOutOfApp -> intentHandler.exitApplication() is VaultEvent.ShowToast -> { Toast - .makeText(context, event.message, Toast.LENGTH_SHORT) + .makeText(context, event.message(context.resources), Toast.LENGTH_SHORT) .show() } } @@ -206,6 +209,14 @@ private fun VaultScreenScaffold( // Dynamic dialogs when (val dialog = state.dialog) { + is VaultState.DialogState.Syncing -> { + BitwardenLoadingDialog( + visibilityState = LoadingDialogState.Shown( + text = R.string.syncing.asText(), + ), + ) + } + is VaultState.DialogState.Error -> { BitwardenBasicDialog( visibilityState = BasicDialogState.Shown( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index 70c4e7c0c..ee8705ddd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -186,6 +186,9 @@ class VaultViewModel @Inject constructor( } private fun handleSyncClick() { + mutableStateFlow.update { + it.copy(dialog = VaultState.DialogState.Syncing) + } vaultRepository.sync() } @@ -285,8 +288,18 @@ class VaultViewModel @Inject constructor( } private fun vaultLoadedReceive(vaultData: DataState.Loaded) { + if (state.dialog == VaultState.DialogState.Syncing) { + sendEvent( + VaultEvent.ShowToast( + message = R.string.syncing_complete.asText(), + ), + ) + } mutableStateFlow.update { - it.copy(viewState = vaultData.data.toViewState(vaultFilterTypeOrDefault)) + it.copy( + viewState = vaultData.data.toViewState(vaultFilterTypeOrDefault), + dialog = null, + ) } } @@ -308,7 +321,6 @@ class VaultViewModel @Inject constructor( mutableStateFlow.update { it.copy(viewState = vaultData.data.toViewState(vaultFilterTypeOrDefault)) } - sendEvent(VaultEvent.ShowToast(message = "Refreshing")) } //endregion VaultAction Handlers @@ -550,6 +562,12 @@ data class VaultState( */ sealed class DialogState : Parcelable { + /** + * Represents a dialog indication and ongoing manual sync. + */ + @Parcelize + data object Syncing : DialogState() + /** * Represents an error dialog with the given [title] and [message]. */ @@ -612,7 +630,7 @@ sealed class VaultEvent { /** * Show a toast with the given [message]. */ - data class ShowToast(val message: String) : VaultEvent() + data class ShowToast(val message: Text) : VaultEvent() } /** @@ -778,6 +796,7 @@ private fun MutableStateFlow.updateToErrorStateOrDialog( viewState = VaultState.ViewState.Error( message = errorMessage, ), + dialog = null, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index e58cbe1fd..a53013000 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -281,9 +281,15 @@ class VaultViewModelTest : BaseViewModelTest() { } @Test - fun `on SyncClick should call sync on the VaultRepository`() { + fun `on SyncClick should call sync on the VaultRepository and show the syncing dialog`() { val viewModel = createViewModel() viewModel.trySendAction(VaultAction.SyncClick) + assertEquals( + DEFAULT_STATE.copy( + dialog = VaultState.DialogState.Syncing, + ), + viewModel.stateFlow.value, + ) verify { vaultRepository.sync() } @@ -405,6 +411,45 @@ class VaultViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `vaultDataStateFlow Loaded with items when manually syncing with the sync button should update state to Content and show a success Toast`() = + runTest { + val expectedState = createMockVaultState( + viewState = VaultState.ViewState.Content( + loginItemsCount = 1, + cardItemsCount = 0, + identityItemsCount = 0, + secureNoteItemsCount = 0, + favoriteItems = listOf(), + folderItems = listOf(), + collectionItems = listOf(), + noFolderItems = listOf(), + trashItemsCount = 0, + ), + ) + val viewModel = createViewModel() + viewModel.trySendAction(VaultAction.SyncClick) + + viewModel.eventFlow.test { + mutableVaultDataStateFlow.tryEmit( + value = DataState.Loaded( + data = VaultData( + cipherViewList = listOf(createMockCipherView(number = 1)), + collectionViewList = emptyList(), + folderViewList = emptyList(), + ), + ), + ) + + assertEquals(expectedState, viewModel.stateFlow.value) + assertEquals( + VaultEvent.ShowToast(R.string.syncing_complete.asText()), + awaitItem(), + ) + } + } + @Test fun `vaultDataStateFlow Loaded with empty items should update state to NoItems`() = runTest { mutableVaultDataStateFlow.tryEmit( @@ -423,6 +468,95 @@ class VaultViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `vaultDataStateFlow Loaded with empty items when manually syncing with the sync button should update state to NoItems and show a success Toast`() = + runTest { + val expectedState = createMockVaultState( + viewState = VaultState.ViewState.NoItems, + ) + val viewModel = createViewModel() + viewModel.trySendAction(VaultAction.SyncClick) + + viewModel.eventFlow.test { + mutableVaultDataStateFlow.value = DataState.Loaded( + data = VaultData( + cipherViewList = emptyList(), + collectionViewList = emptyList(), + folderViewList = emptyList(), + ), + ) + + assertEquals(expectedState, viewModel.stateFlow.value) + assertEquals( + VaultEvent.ShowToast(R.string.syncing_complete.asText()), + awaitItem(), + ) + } + } + + @Test + fun `vaultDataStateFlow Pending with items should update state to Content`() { + mutableVaultDataStateFlow.tryEmit( + value = DataState.Pending( + data = VaultData( + cipherViewList = listOf(createMockCipherView(number = 1)), + collectionViewList = listOf(createMockCollectionView(number = 1)), + folderViewList = listOf(createMockFolderView(number = 1)), + ), + ), + ) + + val viewModel = createViewModel() + + assertEquals( + createMockVaultState( + viewState = VaultState.ViewState.Content( + loginItemsCount = 1, + cardItemsCount = 0, + identityItemsCount = 0, + secureNoteItemsCount = 0, + favoriteItems = listOf(), + folderItems = listOf( + VaultState.ViewState.FolderItem( + id = "mockId-1", + name = "mockName-1".asText(), + itemCount = 1, + ), + ), + collectionItems = listOf( + VaultState.ViewState.CollectionItem( + id = "mockId-1", + name = "mockName-1", + itemCount = 1, + ), + ), + noFolderItems = listOf(), + trashItemsCount = 0, + ), + ), + viewModel.stateFlow.value, + ) + } + + @Test + fun `vaultDataStateFlow Pending with empty items should update state to NoItems`() = runTest { + mutableVaultDataStateFlow.tryEmit( + value = DataState.Pending( + data = VaultData( + cipherViewList = emptyList(), + collectionViewList = emptyList(), + folderViewList = emptyList(), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + createMockVaultState(viewState = VaultState.ViewState.NoItems), + viewModel.stateFlow.value, + ) + } + @Test fun `vaultDataStateFlow Loading should update state to Loading`() = runTest { mutableVaultDataStateFlow.tryEmit(value = DataState.Loading)