diff --git a/feature/bookmarks/build.gradle.kts b/feature/bookmarks/build.gradle.kts index 5dfd7e014..667e674ec 100644 --- a/feature/bookmarks/build.gradle.kts +++ b/feature/bookmarks/build.gradle.kts @@ -14,8 +14,6 @@ * limitations under the License. */ -import com.android.build.api.dsl.ManagedVirtualDevice - plugins { id("nowinandroid.android.feature") id("nowinandroid.android.library.compose") @@ -28,4 +26,4 @@ android { dependencies { implementation(libs.androidx.compose.material3.windowSizeClass) -} \ No newline at end of file +} diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt index 2ed6a76b3..e2eb4524b 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreen.kt @@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks import androidx.annotation.VisibleForTesting import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -34,13 +35,23 @@ import androidx.compose.foundation.lazy.grid.GridCells.Adaptive import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration.Short +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult.ActionPerformed import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -50,6 +61,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalTintTheme @@ -76,12 +89,16 @@ internal fun BookmarksRoute( onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) }, onTopicClick = onTopicClick, modifier = modifier, + shouldDisplayUndoBookmark = viewModel.shouldDisplayUndoBookmark, + undoBookmarkRemoval = viewModel::undoBookmarkRemoval, + clearUndoState = viewModel::clearUndoState, ) } /** * Displays the user's bookmarked articles. Includes support for loading and empty states. */ +@OptIn(ExperimentalMaterial3Api::class) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @Composable internal fun BookmarksScreen( @@ -90,13 +107,51 @@ internal fun BookmarksScreen( onNewsResourceViewed: (String) -> Unit, onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, + shouldDisplayUndoBookmark: Boolean = false, + undoBookmarkRemoval: () -> Unit = {}, + clearUndoState: () -> Unit = {}, ) { - when (feedState) { - Loading -> LoadingState(modifier) - is Success -> if (feedState.feed.isNotEmpty()) { - BookmarksGrid(feedState, removeFromBookmarks, onNewsResourceViewed, onTopicClick, modifier) - } else { - EmptyState(modifier) + val bookmarkRemovedMessage = stringResource(id = R.string.bookmark_removed) + val undoText = stringResource(id = R.string.undo) + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(shouldDisplayUndoBookmark) { + if (shouldDisplayUndoBookmark) { + val snackBarResult = snackbarHostState.showSnackbar( + message = bookmarkRemovedMessage, + actionLabel = undoText, + duration = Short, + ) + when (snackBarResult) { + ActionPerformed -> { undoBookmarkRemoval() } + else -> { clearUndoState() } + } + } + } + + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_STOP) { + clearUndoState() + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + + Scaffold(snackbarHost = { SnackbarHost(hostState = snackbarHostState) }) { + Box( + modifier = Modifier.padding(it).fillMaxSize(), + ) { + when (feedState) { + Loading -> LoadingState(modifier) + is Success -> if (feedState.feed.isNotEmpty()) { + BookmarksGrid(feedState, removeFromBookmarks, onNewsResourceViewed, onTopicClick, modifier) + } else { + EmptyState(modifier) + } + } } } TrackScreenViewEvent(screenName = "Saved") diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt index 8a1869322..7b6cac76a 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt @@ -16,6 +16,9 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository @@ -38,6 +41,9 @@ class BookmarksViewModel @Inject constructor( userNewsResourceRepository: UserNewsResourceRepository, ) : ViewModel() { + var shouldDisplayUndoBookmark by mutableStateOf(false) + private var lastRemovedBookmarkId: String? = null + val feedUiState: StateFlow = userNewsResourceRepository.observeAllBookmarked() .map, NewsFeedUiState>(NewsFeedUiState::Success) @@ -50,6 +56,8 @@ class BookmarksViewModel @Inject constructor( fun removeFromSavedResources(newsResourceId: String) { viewModelScope.launch { + shouldDisplayUndoBookmark = true + lastRemovedBookmarkId = newsResourceId userDataRepository.updateNewsResourceBookmark(newsResourceId, false) } } @@ -59,4 +67,18 @@ class BookmarksViewModel @Inject constructor( userDataRepository.setNewsResourceViewed(newsResourceId, viewed) } } + + fun undoBookmarkRemoval() { + viewModelScope.launch { + lastRemovedBookmarkId?.let { + userDataRepository.updateNewsResourceBookmark(it, true) + } + } + clearUndoState() + } + + fun clearUndoState() { + shouldDisplayUndoBookmark = false + lastRemovedBookmarkId = null + } } diff --git a/feature/bookmarks/src/main/res/values/strings.xml b/feature/bookmarks/src/main/res/values/strings.xml index 61781ad6e..2dd36659e 100644 --- a/feature/bookmarks/src/main/res/values/strings.xml +++ b/feature/bookmarks/src/main/res/values/strings.xml @@ -22,4 +22,6 @@ Menu No saved updates Updates you save will be stored here\nto read later - \ No newline at end of file + Bookmark removed + UNDO +