Refactor bookmark approach

pull/2125/head
Rohit Karadkar 3 months ago
parent d5d3a99e46
commit 40805844d0

@ -18,12 +18,13 @@ import androidx.compose.ui.Modifier
@Composable @Composable
fun BookmarkNoteDialog( fun BookmarkNoteDialog(
initialNote: String = "", initialNote: String = "",
onDismiss: (note: String) -> Unit, onDismiss: () -> Unit,
onSave: (note: String) -> Unit,
) { ) {
var note by remember { mutableStateOf(initialNote) } var note by remember { mutableStateOf(initialNote) }
AlertDialog( AlertDialog(
onDismissRequest = { onDismiss(note) }, onDismissRequest = onDismiss,
title = { Text("Add a note") }, title = { Text("Add a note") },
text = { text = {
Column { Column {
@ -42,10 +43,10 @@ fun BookmarkNoteDialog(
} }
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { onDismiss(note) }) { Text("Save") } TextButton(onClick = { onSave(note) }) { Text("Save") }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { onDismiss("") }) { Text("Skip") } TextButton(onClick = onDismiss) { Text("Cancel") }
}, },
) )
} }

@ -106,37 +106,40 @@ internal fun BookmarksScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: BookmarksViewModel = hiltViewModel(), viewModel: BookmarksViewModel = hiltViewModel(),
) { ) {
val feedState by viewModel.feedUiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var editingNoteId by remember { mutableStateOf<String?>(null) } var editingNoteId by remember { mutableStateOf<String?>(null) }
BookmarksScreen( BookmarksScreen(
feedState = feedState, feedState = uiState.feedState,
onShowSnackbar = onShowSnackbar, onShowSnackbar = onShowSnackbar,
removeFromBookmarks = viewModel::removeFromSavedResources, removeFromBookmarks = viewModel::removeFromSavedResources,
onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) }, onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) },
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
modifier = modifier, modifier = modifier,
shouldDisplayUndoBookmark = viewModel.shouldDisplayUndoBookmark, shouldDisplayUndoBookmark = uiState.shouldDisplayUndoBookmark,
undoBookmarkRemoval = viewModel::undoBookmarkRemoval, undoBookmarkRemoval = viewModel::undoBookmarkRemoval,
clearUndoState = viewModel::clearUndoState, clearUndoState = viewModel::clearUndoState,
onEditNote = { editingNoteId = it }, onEditNote = { editingNoteId = it },
isInSelectionMode = viewModel.isInSelectionMode, isInSelectionMode = uiState.isInSelectionMode,
selectedIds = viewModel.selectedIds, selectedIds = uiState.selectedIds,
enterSelectionMode = viewModel::enterSelectionMode, enterSelectionMode = viewModel::enterSelectionMode,
exitSelectionMode = viewModel::exitSelectionMode, exitSelectionMode = viewModel::exitSelectionMode,
toggleSelection = viewModel::toggleSelection, toggleSelection = viewModel::toggleSelection,
selectAll = viewModel::selectAll, selectAll = viewModel::selectAll,
shouldDisplayUndoBulkRemove = viewModel.shouldDisplayUndoBulkRemove, shouldDisplayUndoBulkRemove = uiState.shouldDisplayUndoBulkRemove,
bulkRemovedCount = uiState.bulkRemovedCount,
removeSelected = viewModel::removeSelected, removeSelected = viewModel::removeSelected,
undoBulkRemove = viewModel::undoBulkRemove, undoBulkRemove = viewModel::undoBulkRemove,
clearBulkUndoState = viewModel::clearBulkUndoState, clearBulkUndoState = viewModel::clearBulkUndoState,
) )
editingNoteId?.let { id -> editingNoteId?.let { id ->
val currentNote = (feedState as? Success)?.feed?.find { it.id == id }?.bookmarkNote ?: "" val currentNote =
(uiState.feedState as? Success)?.feed?.find { it.id == id }?.bookmarkNote ?: ""
BookmarkNoteDialog( BookmarkNoteDialog(
initialNote = currentNote, initialNote = currentNote,
onDismiss = { note -> onDismiss = { editingNoteId = null },
onSave = { note ->
viewModel.updateNote(id, note) viewModel.updateNote(id, note)
editingNoteId = null editingNoteId = null
}, },
@ -167,13 +170,13 @@ internal fun BookmarksScreen(
toggleSelection: (String) -> Unit = {}, toggleSelection: (String) -> Unit = {},
selectAll: () -> Unit = {}, selectAll: () -> Unit = {},
shouldDisplayUndoBulkRemove: Boolean = false, shouldDisplayUndoBulkRemove: Boolean = false,
bulkRemovedCount: Int = 0,
removeSelected: () -> Unit = {}, removeSelected: () -> Unit = {},
undoBulkRemove: () -> Unit = {}, undoBulkRemove: () -> Unit = {},
clearBulkUndoState: () -> Unit = {}, clearBulkUndoState: () -> Unit = {},
) { ) {
val bookmarkRemovedMessage = stringResource(id = R.string.feature_bookmarks_api_removed) val bookmarkRemovedMessage = stringResource(id = R.string.feature_bookmarks_api_removed)
val undoText = stringResource(id = R.string.feature_bookmarks_api_undo) val undoText = stringResource(id = R.string.feature_bookmarks_api_undo)
val removedCount = remember { androidx.compose.runtime.mutableIntStateOf(0) }
BackHandler(enabled = isInSelectionMode) { BackHandler(enabled = isInSelectionMode) {
exitSelectionMode() exitSelectionMode()
@ -193,7 +196,7 @@ internal fun BookmarksScreen(
LaunchedEffect(shouldDisplayUndoBulkRemove) { LaunchedEffect(shouldDisplayUndoBulkRemove) {
if (shouldDisplayUndoBulkRemove) { if (shouldDisplayUndoBulkRemove) {
val result = onShowSnackbar( val result = onShowSnackbar(
"${removedCount.intValue} bookmarks removed", "$bulkRemovedCount bookmarks removed",
undoText, undoText,
) )
if (result) { if (result) {
@ -253,10 +256,7 @@ internal fun BookmarksScreen(
) )
TextButton(onClick = selectAll) { Text("All") } TextButton(onClick = selectAll) { Text("All") }
Button( Button(
onClick = { onClick = removeSelected,
removedCount.intValue = selectedIds.size
removeSelected()
},
enabled = selectedIds.isNotEmpty(), enabled = selectedIds.isNotEmpty(),
) { ) {
Text("Remove") Text("Remove")

@ -0,0 +1,29 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.feature.bookmarks.impl
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
data class BookmarksUiState(
val feedState: NewsFeedUiState = Loading,
val isInSelectionMode: Boolean = false,
val selectedIds: Set<String> = emptySet(),
val shouldDisplayUndoBookmark: Boolean = false,
val shouldDisplayUndoBulkRemove: Boolean = false,
val bulkRemovedCount: Int = 0,
)

@ -16,9 +16,6 @@
package com.google.samples.apps.nowinandroid.feature.bookmarks.impl package com.google.samples.apps.nowinandroid.feature.bookmarks.impl
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
@ -27,11 +24,14 @@ import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -41,48 +41,48 @@ class BookmarksViewModel @Inject constructor(
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
) : ViewModel() { ) : ViewModel() {
var shouldDisplayUndoBookmark by mutableStateOf(false) private val _state = MutableStateFlow(BookmarksUiState())
private var lastRemovedBookmarkId: String? = null private var lastRemovedBookmarkId: String? = null
private var bulkRemoveSnapshot: List<Pair<String, String?>> = emptyList()
var isInSelectionMode by mutableStateOf(false) val uiState: StateFlow<BookmarksUiState> = combine(
private set userNewsResourceRepository.observeAllBookmarked()
.map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
var selectedIds by mutableStateOf(emptySet<String>()) .onStart { emit(Loading) },
private set _state,
) { feedState, state ->
state.copy(feedState = feedState)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = BookmarksUiState(),
)
fun enterSelectionMode(initialId: String) { fun enterSelectionMode(initialId: String) {
isInSelectionMode = true _state.update { it.copy(isInSelectionMode = true, selectedIds = setOf(initialId)) }
selectedIds = setOf(initialId)
} }
fun exitSelectionMode() { fun exitSelectionMode() {
isInSelectionMode = false _state.update { it.copy(isInSelectionMode = false, selectedIds = emptySet()) }
selectedIds = emptySet()
} }
fun toggleSelection(id: String) { fun toggleSelection(id: String) {
selectedIds = if (id in selectedIds) selectedIds - id else selectedIds + id _state.update { current ->
val updated =
if (id in current.selectedIds) current.selectedIds - id else current.selectedIds + id
current.copy(selectedIds = updated)
}
} }
fun selectAll() { fun selectAll() {
val currentFeed = (feedUiState.value as? NewsFeedUiState.Success)?.feed ?: return val currentFeed = (uiState.value.feedState as? NewsFeedUiState.Success)?.feed ?: return
selectedIds = currentFeed.map { it.id }.toSet() _state.update { it.copy(selectedIds = currentFeed.map { item -> item.id }.toSet()) }
} }
val feedUiState: StateFlow<NewsFeedUiState> =
userNewsResourceRepository.observeAllBookmarked()
.map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
.onStart { emit(Loading) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Loading,
)
fun removeFromSavedResources(newsResourceId: String) { fun removeFromSavedResources(newsResourceId: String) {
lastRemovedBookmarkId = newsResourceId
_state.update { it.copy(shouldDisplayUndoBookmark = true) }
viewModelScope.launch { viewModelScope.launch {
shouldDisplayUndoBookmark = true
lastRemovedBookmarkId = newsResourceId
userDataRepository.setNewsResourceBookmarked(newsResourceId, false) userDataRepository.setNewsResourceBookmarked(newsResourceId, false)
} }
} }
@ -103,28 +103,29 @@ class BookmarksViewModel @Inject constructor(
} }
fun clearUndoState() { fun clearUndoState() {
shouldDisplayUndoBookmark = false
lastRemovedBookmarkId = null lastRemovedBookmarkId = null
_state.update { it.copy(shouldDisplayUndoBookmark = false) }
} }
var shouldDisplayUndoBulkRemove by mutableStateOf(false)
private set
private var bulkRemoveSnapshot: List<Pair<String, String?>> = emptyList()
fun removeSelected() { fun removeSelected() {
val currentFeed = (feedUiState.value as? NewsFeedUiState.Success)?.feed ?: return val currentFeed = (uiState.value.feedState as? NewsFeedUiState.Success)?.feed ?: return
bulkRemoveSnapshot = selectedIds.map { id -> val toRemove = _state.value.selectedIds
bulkRemoveSnapshot = toRemove.map { id ->
id to currentFeed.find { it.id == id }?.bookmarkNote id to currentFeed.find { it.id == id }?.bookmarkNote
} }
val toRemove = selectedIds.toSet()
viewModelScope.launch { viewModelScope.launch {
toRemove.forEach { id -> toRemove.forEach { id ->
userDataRepository.setNewsResourceBookmarked(id, false) userDataRepository.setNewsResourceBookmarked(id, false)
} }
} }
exitSelectionMode() _state.update {
shouldDisplayUndoBulkRemove = true it.copy(
isInSelectionMode = false,
selectedIds = emptySet(),
shouldDisplayUndoBulkRemove = true,
bulkRemovedCount = toRemove.size,
)
}
} }
fun undoBulkRemove() { fun undoBulkRemove() {
@ -140,8 +141,8 @@ class BookmarksViewModel @Inject constructor(
} }
fun clearBulkUndoState() { fun clearBulkUndoState() {
shouldDisplayUndoBulkRemove = false
bulkRemoveSnapshot = emptyList() bulkRemoveSnapshot = emptyList()
_state.update { it.copy(shouldDisplayUndoBulkRemove = false, bulkRemovedCount = 0) }
} }
fun updateNote(newsResourceId: String, note: String) { fun updateNote(newsResourceId: String, note: String) {

@ -23,7 +23,6 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
import com.google.samples.apps.nowinandroid.feature.bookmarks.impl.BookmarksViewModel
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
@ -62,23 +61,23 @@ class BookmarksViewModelTest {
@Test @Test
fun stateIsInitiallyLoading() = runTest { fun stateIsInitiallyLoading() = runTest {
assertEquals(Loading, viewModel.feedUiState.value) assertEquals(Loading, viewModel.uiState.value.feedState)
} }
@Test @Test
fun oneBookmark_showsInFeed() = runTest { fun oneBookmark_showsInFeed() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() } backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
newsRepository.sendNewsResources(newsResourcesTestData) newsRepository.sendNewsResources(newsResourcesTestData)
userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true) userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true)
val item = viewModel.feedUiState.value val feedState = viewModel.uiState.value.feedState
assertIs<Success>(item) assertIs<Success>(feedState)
assertEquals(item.feed.size, 1) assertEquals(feedState.feed.size, 1)
} }
@Test @Test
fun oneBookmark_whenRemoving_removesFromFeed() = runTest { fun oneBookmark_whenRemoving_removesFromFeed() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() } backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
// Set the news resources to be used by this test // Set the news resources to be used by this test
newsRepository.sendNewsResources(newsResourcesTestData) newsRepository.sendNewsResources(newsResourcesTestData)
// Start with the resource saved // Start with the resource saved
@ -86,59 +85,62 @@ class BookmarksViewModelTest {
// Use viewModel to remove saved resource // Use viewModel to remove saved resource
viewModel.removeFromSavedResources(newsResourcesTestData[0].id) viewModel.removeFromSavedResources(newsResourcesTestData[0].id)
// Verify list of saved resources is now empty // Verify list of saved resources is now empty
val item = viewModel.feedUiState.value val feedState = viewModel.uiState.value.feedState
assertIs<Success>(item) assertIs<Success>(feedState)
assertEquals(item.feed.size, 0) assertEquals(feedState.feed.size, 0)
assertTrue(viewModel.shouldDisplayUndoBookmark) assertTrue(viewModel.uiState.value.shouldDisplayUndoBookmark)
} }
@Test @Test
fun feedUiState_resourceIsViewed_setResourcesViewed() = runTest { fun feedUiState_resourceIsViewed_setResourcesViewed() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() } backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
// Given // Given
newsRepository.sendNewsResources(newsResourcesTestData) newsRepository.sendNewsResources(newsResourcesTestData)
userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true) userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true)
val itemBeforeViewed = viewModel.feedUiState.value val feedStateBeforeViewed = viewModel.uiState.value.feedState
assertIs<Success>(itemBeforeViewed) assertIs<Success>(feedStateBeforeViewed)
assertFalse(itemBeforeViewed.feed.first().hasBeenViewed) assertFalse(feedStateBeforeViewed.feed.first().hasBeenViewed)
// When // When
viewModel.setNewsResourceViewed(newsResourcesTestData[0].id, true) viewModel.setNewsResourceViewed(newsResourcesTestData[0].id, true)
// Then // Then
val item = viewModel.feedUiState.value val feedState = viewModel.uiState.value.feedState
assertIs<Success>(item) assertIs<Success>(feedState)
assertTrue(item.feed.first().hasBeenViewed) assertTrue(feedState.feed.first().hasBeenViewed)
} }
@Test @Test
fun enterSelectionMode_setsSelectedId() = runTest { fun enterSelectionMode_setsSelectedId() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
viewModel.enterSelectionMode("news1") viewModel.enterSelectionMode("news1")
assertTrue(viewModel.isInSelectionMode) assertTrue(viewModel.uiState.value.isInSelectionMode)
assertEquals(setOf("news1"), viewModel.selectedIds) assertEquals(setOf("news1"), viewModel.uiState.value.selectedIds)
} }
@Test @Test
fun toggleSelection_addsAndRemovesId() = runTest { fun toggleSelection_addsAndRemovesId() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
viewModel.enterSelectionMode("news1") viewModel.enterSelectionMode("news1")
viewModel.toggleSelection("news2") viewModel.toggleSelection("news2")
assertEquals(setOf("news1", "news2"), viewModel.selectedIds) assertEquals(setOf("news1", "news2"), viewModel.uiState.value.selectedIds)
viewModel.toggleSelection("news1") viewModel.toggleSelection("news1")
assertEquals(setOf("news2"), viewModel.selectedIds) assertEquals(setOf("news2"), viewModel.uiState.value.selectedIds)
} }
@Test @Test
fun exitSelectionMode_clearsState() = runTest { fun exitSelectionMode_clearsState() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
viewModel.enterSelectionMode("news1") viewModel.enterSelectionMode("news1")
viewModel.exitSelectionMode() viewModel.exitSelectionMode()
assertFalse(viewModel.isInSelectionMode) assertFalse(viewModel.uiState.value.isInSelectionMode)
assertTrue(viewModel.selectedIds.isEmpty()) assertTrue(viewModel.uiState.value.selectedIds.isEmpty())
} }
@Test @Test
fun removeSelected_capturesSnapshotBeforeRemoval() = runTest { fun removeSelected_capturesSnapshotBeforeRemoval() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() } backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
newsRepository.sendNewsResources(newsResourcesTestData) newsRepository.sendNewsResources(newsResourcesTestData)
userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true) userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true)
@ -149,31 +151,31 @@ class BookmarksViewModelTest {
viewModel.removeSelected() viewModel.removeSelected()
assertTrue(viewModel.shouldDisplayUndoBulkRemove) assertTrue(viewModel.uiState.value.shouldDisplayUndoBulkRemove)
assertFalse(viewModel.isInSelectionMode) assertFalse(viewModel.uiState.value.isInSelectionMode)
assertTrue(viewModel.selectedIds.isEmpty()) assertTrue(viewModel.uiState.value.selectedIds.isEmpty())
} }
@Test @Test
fun feedUiState_undoneBookmarkRemoval_bookmarkIsRestored() = runTest { fun feedUiState_undoneBookmarkRemoval_bookmarkIsRestored() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() } backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
// Given // Given
newsRepository.sendNewsResources(newsResourcesTestData) newsRepository.sendNewsResources(newsResourcesTestData)
userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true) userDataRepository.setNewsResourceBookmarked(newsResourcesTestData[0].id, true)
viewModel.removeFromSavedResources(newsResourcesTestData[0].id) viewModel.removeFromSavedResources(newsResourcesTestData[0].id)
assertTrue(viewModel.shouldDisplayUndoBookmark) assertTrue(viewModel.uiState.value.shouldDisplayUndoBookmark)
val itemBeforeUndo = viewModel.feedUiState.value val feedStateBeforeUndo = viewModel.uiState.value.feedState
assertIs<Success>(itemBeforeUndo) assertIs<Success>(feedStateBeforeUndo)
assertEquals(0, itemBeforeUndo.feed.size) assertEquals(0, feedStateBeforeUndo.feed.size)
// When // When
viewModel.undoBookmarkRemoval() viewModel.undoBookmarkRemoval()
// Then // Then
assertFalse(viewModel.shouldDisplayUndoBookmark) assertFalse(viewModel.uiState.value.shouldDisplayUndoBookmark)
val item = viewModel.feedUiState.value val feedState = viewModel.uiState.value.feedState
assertIs<Success>(item) assertIs<Success>(feedState)
assertEquals(1, item.feed.size) assertEquals(1, feedState.feed.size)
} }
} }

@ -89,7 +89,6 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus.Denied import com.google.accompanist.permissions.PermissionStatus.Denied
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage
import com.google.samples.apps.nowinandroid.core.ui.BookmarkNoteDialog
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverlayLoadingWheel
@ -100,6 +99,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollba
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.BookmarkNoteDialog
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
@ -120,7 +120,7 @@ fun ForYouScreen(
val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle() val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle()
val deepLinkedUserNewsResource by viewModel.deepLinkedNewsResource.collectAsStateWithLifecycle() val deepLinkedUserNewsResource by viewModel.deepLinkedNewsResource.collectAsStateWithLifecycle()
var pendingBookmarkId by remember { mutableStateOf<String?>(null) } var pendingBookmarkNoteId by remember { mutableStateOf<String?>(null) }
ForYouScreen( ForYouScreen(
isSyncing = isSyncing, isSyncing = isSyncing,
@ -132,21 +132,21 @@ fun ForYouScreen(
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
saveFollowedTopics = viewModel::dismissOnboarding, saveFollowedTopics = viewModel::dismissOnboarding,
onNewsResourcesCheckedChanged = { id, checked -> onNewsResourcesCheckedChanged = { id, checked ->
viewModel.updateNewsResourceSaved(id, checked)
if (checked) { if (checked) {
pendingBookmarkId = id pendingBookmarkNoteId = id
} else {
viewModel.updateNewsResourceSaved(id, false)
} }
}, },
onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) }, onNewsResourceViewed = { viewModel.setNewsResourceViewed(it, true) },
modifier = modifier, modifier = modifier,
) )
pendingBookmarkId?.let { id -> pendingBookmarkNoteId?.let { id ->
BookmarkNoteDialog( BookmarkNoteDialog(
onDismiss = { note -> onDismiss = { pendingBookmarkNoteId = null },
viewModel.bookmarkWithNote(id, note) onSave = { note ->
pendingBookmarkId = null viewModel.updateNote(id, note)
pendingBookmarkNoteId = null
}, },
) )
} }
@ -286,7 +286,7 @@ private fun LazyStaggeredGridScope.onboarding(
OnboardingUiState.Loading, OnboardingUiState.Loading,
OnboardingUiState.LoadFailed, OnboardingUiState.LoadFailed,
OnboardingUiState.NotShown, OnboardingUiState.NotShown,
-> Unit -> Unit
is OnboardingUiState.Shown -> { is OnboardingUiState.Shown -> {
item(span = StaggeredGridItemSpan.FullLine, contentType = "onboarding") { item(span = StaggeredGridItemSpan.FullLine, contentType = "onboarding") {
@ -515,7 +515,7 @@ private fun feedItemsSize(
OnboardingUiState.Loading, OnboardingUiState.Loading,
OnboardingUiState.LoadFailed, OnboardingUiState.LoadFailed,
OnboardingUiState.NotShown, OnboardingUiState.NotShown,
-> 0 -> 0
is OnboardingUiState.Shown -> 1 is OnboardingUiState.Shown -> 1
} }

@ -121,12 +121,9 @@ class ForYouViewModel @Inject constructor(
} }
} }
fun bookmarkWithNote(newsResourceId: String, note: String) { fun updateNote(newsResourceId: String, note: String) {
viewModelScope.launch { viewModelScope.launch {
userDataRepository.setNewsResourceBookmarked(newsResourceId, true) userDataRepository.setBookmarkNote(newsResourceId, note)
if (note.isNotBlank()) {
userDataRepository.setBookmarkNote(newsResourceId, note)
}
} }
} }

@ -110,7 +110,7 @@ internal fun SearchScreen(
val searchResultUiState by searchViewModel.searchResultUiState.collectAsStateWithLifecycle() val searchResultUiState by searchViewModel.searchResultUiState.collectAsStateWithLifecycle()
val searchQuery by searchViewModel.searchQuery.collectAsStateWithLifecycle() val searchQuery by searchViewModel.searchQuery.collectAsStateWithLifecycle()
var pendingBookmarkId by remember { mutableStateOf<String?>(null) } var pendingBookmarkNoteId by remember { mutableStateOf<String?>(null) }
SearchScreen( SearchScreen(
modifier = modifier, modifier = modifier,
@ -122,7 +122,8 @@ internal fun SearchScreen(
onClearRecentSearches = searchViewModel::clearRecentSearches, onClearRecentSearches = searchViewModel::clearRecentSearches,
onNewsResourcesCheckedChanged = { id, checked -> onNewsResourcesCheckedChanged = { id, checked ->
if (checked) { if (checked) {
pendingBookmarkId = id searchViewModel.setNewsResourceBookmarked(id, true)
pendingBookmarkNoteId = id
} else { } else {
searchViewModel.setNewsResourceBookmarked(id, false) searchViewModel.setNewsResourceBookmarked(id, false)
} }
@ -134,11 +135,12 @@ internal fun SearchScreen(
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
) )
pendingBookmarkId?.let { id -> pendingBookmarkNoteId?.let { id ->
BookmarkNoteDialog( BookmarkNoteDialog(
onDismiss = { note -> onDismiss = { pendingBookmarkNoteId = null },
searchViewModel.bookmarkWithNote(id, note) onSave = { note ->
pendingBookmarkId = null searchViewModel.updateNote(id, note)
pendingBookmarkNoteId = null
}, },
) )
} }

@ -121,12 +121,9 @@ class SearchViewModel @Inject constructor(
} }
} }
fun bookmarkWithNote(newsResourceId: String, note: String) { fun updateNote(newsResourceId: String, note: String) {
viewModelScope.launch { viewModelScope.launch {
userDataRepository.setNewsResourceBookmarked(newsResourceId, true) userDataRepository.setBookmarkNote(newsResourceId, note)
if (note.isNotBlank()) {
userDataRepository.setBookmarkNote(newsResourceId, note)
}
} }
} }

Loading…
Cancel
Save