From ad557a39cfb366fbddd29abaf588a3ab147b20d3 Mon Sep 17 00:00:00 2001 From: Rohit Karadkar Date: Tue, 9 Jun 2026 11:20:23 +0530 Subject: [PATCH] feat: bulk remove bookmarks with undo snackbar restoring bookmarks and notes Co-Authored-By: Claude --- .../feature/bookmarks/impl/BookmarksScreen.kt | 28 +++++++++++++- .../bookmarks/impl/BookmarksViewModel.kt | 37 +++++++++++++++++++ .../bookmarks/impl/BookmarksViewModelTest.kt | 18 +++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreen.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreen.kt index 81ec742cb..20abe5ca8 100644 --- a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreen.kt +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksScreen.kt @@ -126,6 +126,10 @@ internal fun BookmarksScreen( exitSelectionMode = viewModel::exitSelectionMode, toggleSelection = viewModel::toggleSelection, selectAll = viewModel::selectAll, + shouldDisplayUndoBulkRemove = viewModel.shouldDisplayUndoBulkRemove, + removeSelected = viewModel::removeSelected, + undoBulkRemove = viewModel::undoBulkRemove, + clearBulkUndoState = viewModel::clearBulkUndoState, ) editingNoteId?.let { id -> @@ -163,9 +167,14 @@ internal fun BookmarksScreen( exitSelectionMode: () -> Unit = {}, toggleSelection: (String) -> Unit = {}, selectAll: () -> Unit = {}, + shouldDisplayUndoBulkRemove: Boolean = false, + removeSelected: () -> Unit = {}, + undoBulkRemove: () -> Unit = {}, + clearBulkUndoState: () -> Unit = {}, ) { val bookmarkRemovedMessage = stringResource(id = R.string.feature_bookmarks_api_removed) val undoText = stringResource(id = R.string.feature_bookmarks_api_undo) + val removedCount = remember { androidx.compose.runtime.mutableIntStateOf(0) } BackHandler(enabled = isInSelectionMode) { exitSelectionMode() @@ -182,6 +191,20 @@ internal fun BookmarksScreen( } } + LaunchedEffect(shouldDisplayUndoBulkRemove) { + if (shouldDisplayUndoBulkRemove) { + val result = onShowSnackbar( + "${removedCount.intValue} bookmarks removed", + undoText, + ) + if (result) { + undoBulkRemove() + } else { + clearBulkUndoState() + } + } + } + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { clearUndoState() } @@ -222,7 +245,10 @@ internal fun BookmarksScreen( if (isInSelectionMode && selectedIds.isNotEmpty()) { Button( - onClick = { selectedIds.forEach { removeFromBookmarks(it) } }, + onClick = { + removedCount.intValue = selectedIds.size + removeSelected() + }, modifier = Modifier .align(Alignment.BottomCenter) .padding(16.dp), diff --git a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModel.kt b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModel.kt index 243f15bc2..f260e0bd5 100644 --- a/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModel.kt +++ b/feature/bookmarks/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModel.kt @@ -107,6 +107,43 @@ class BookmarksViewModel @Inject constructor( lastRemovedBookmarkId = null } + var shouldDisplayUndoBulkRemove by mutableStateOf(false) + private set + + private var bulkRemoveSnapshot: List> = emptyList() + + fun removeSelected() { + val currentFeed = (feedUiState.value as? NewsFeedUiState.Success)?.feed ?: return + bulkRemoveSnapshot = selectedIds.map { id -> + id to currentFeed.find { it.id == id }?.bookmarkNote + } + val toRemove = selectedIds.toSet() + viewModelScope.launch { + toRemove.forEach { id -> + userDataRepository.setNewsResourceBookmarked(id, false) + } + } + exitSelectionMode() + shouldDisplayUndoBulkRemove = true + } + + fun undoBulkRemove() { + viewModelScope.launch { + bulkRemoveSnapshot.forEach { (id, note) -> + userDataRepository.setNewsResourceBookmarked(id, true) + if (!note.isNullOrBlank()) { + userDataRepository.setBookmarkNote(id, note) + } + } + } + clearBulkUndoState() + } + + fun clearBulkUndoState() { + shouldDisplayUndoBulkRemove = false + bulkRemoveSnapshot = emptyList() + } + fun updateNote(newsResourceId: String, note: String) { viewModelScope.launch { if (note.isNotBlank()) { diff --git a/feature/bookmarks/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModelTest.kt b/feature/bookmarks/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModelTest.kt index 43dcc5214..16c9f7853 100644 --- a/feature/bookmarks/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModelTest.kt +++ b/feature/bookmarks/impl/src/test/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/impl/BookmarksViewModelTest.kt @@ -136,6 +136,24 @@ class BookmarksViewModelTest { assertTrue(viewModel.selectedIds.isEmpty()) } + @Test + fun removeSelected_capturesSnapshotBeforeRemoval() = runTest { + backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() } + + newsRepository.sendNewsResources(newsResourcesTestData) + userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true) + userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[1].id, true) + + viewModel.enterSelectionMode(newsResourcesTestData[0].id) + viewModel.toggleSelection(newsResourcesTestData[1].id) + + viewModel.removeSelected() + + assertTrue(viewModel.shouldDisplayUndoBulkRemove) + assertFalse(viewModel.isInSelectionMode) + assertTrue(viewModel.selectedIds.isEmpty()) + } + @Test fun feedUiState_undoneBookmarkRemoval_bookmarkIsRestored() = runTest { backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() }