diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesUseCase.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCase.kt similarity index 62% rename from core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesUseCase.kt rename to core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCase.kt index 4f0cb62bc..07cd6d856 100644 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesUseCase.kt +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCase.kt @@ -18,52 +18,43 @@ package com.google.samples.apps.nowinandroid.core.domain import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import com.google.samples.apps.nowinandroid.core.model.data.UserData import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNot -import kotlinx.coroutines.flow.map /** * A use case responsible for obtaining news resources with their associated bookmarked (also known * as "saved") state. */ -class GetSaveableNewsResourcesUseCase @Inject constructor( +class GetUserNewsResourcesUseCase @Inject constructor( private val newsRepository: NewsRepository, - userDataRepository: UserDataRepository + private val userDataRepository: UserDataRepository ) { - - private val bookmarkedNewsResources = userDataRepository.userData.map { - it.bookmarkedNewsResources - } - /** - * Returns a list of SaveableNewsResources which match the supplied set of topic ids. + * Returns a list of UserNewsResources which match the supplied set of topic ids. * * @param filterTopicIds - A set of topic ids used to filter the list of news resources. If * this is empty the list of news resources will not be filtered. */ operator fun invoke( filterTopicIds: Set = emptySet() - ): Flow> = + ): Flow> = if (filterTopicIds.isEmpty()) { newsRepository.getNewsResources() } else { newsRepository.getNewsResources(filterTopicIds = filterTopicIds) - }.mapToSaveableNewsResources(bookmarkedNewsResources) + }.mapToUserNewsResources(userDataRepository.userData) } -private fun Flow>.mapToSaveableNewsResources( - savedNewsResourceIdsStream: Flow> -): Flow> = +private fun Flow>.mapToUserNewsResources( + userDataStream: Flow +): Flow> = filterNot { it.isEmpty() } - .combine(savedNewsResourceIdsStream) { newsResources, savedNewsResourceIds -> - newsResources.map { newsResource -> - SaveableNewsResource( - newsResource = newsResource, - isSaved = savedNewsResourceIds.contains(newsResource.id) - ) - } + .combine(userDataStream) { newsResources, userData -> + newsResources.mapToUserNewsResources(userData) } diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableTopic.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableTopic.kt index 87a77daa4..4071eb1aa 100644 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableTopic.kt +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/FollowableTopic.kt @@ -17,11 +17,27 @@ package com.google.samples.apps.nowinandroid.core.domain.model import com.google.samples.apps.nowinandroid.core.model.data.Topic +import com.google.samples.apps.nowinandroid.core.model.data.previewTopics /** * A [topic] with the additional information for whether or not it is followed. */ -data class FollowableTopic( +data class FollowableTopic( // TODO consider changing to UserTopic and flattening val topic: Topic, val isFollowed: Boolean ) + +val previewFollowableTopics = listOf( + FollowableTopic( + previewTopics[0], + isFollowed = false + ), + FollowableTopic( + previewTopics[1], + isFollowed = true + ), + FollowableTopic( + previewTopics[2], + isFollowed = false + ) +) diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/SaveableNewsResource.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/SaveableNewsResource.kt deleted file mode 100644 index 6850d421f..000000000 --- a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/SaveableNewsResource.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.core.domain.model - -import com.google.samples.apps.nowinandroid.core.model.data.NewsResource - -/** - * A [NewsResource] with the additional information for whether it is saved. - */ -data class SaveableNewsResource( - val newsResource: NewsResource, - val isSaved: Boolean, -) diff --git a/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/UserNewsResource.kt b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/UserNewsResource.kt new file mode 100644 index 000000000..872f93f16 --- /dev/null +++ b/core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/model/UserNewsResource.kt @@ -0,0 +1,129 @@ +/* + * 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.core.domain.model + +import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType +import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Codelab +import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Unknown +import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video +import com.google.samples.apps.nowinandroid.core.model.data.UserData +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant + +/* ktlint-disable max-line-length */ + +/** + * A [NewsResource] with additional user information such as whether the user is following the + * news resource's topics and whether they have saved (bookmarked) this news resource. + */ +data class UserNewsResource internal constructor( + val id: String, + val title: String, + val content: String, + val url: String, + val headerImageUrl: String?, + val publishDate: Instant, + val type: NewsResourceType, + val followableTopics: List, + val isSaved: Boolean +) { + constructor(newsResource: NewsResource, userData: UserData) : this( + id = newsResource.id, + title = newsResource.title, + content = newsResource.content, + url = newsResource.url, + headerImageUrl = newsResource.headerImageUrl, + publishDate = newsResource.publishDate, + type = newsResource.type, + followableTopics = newsResource.topics.map { topic -> + FollowableTopic( + topic = topic, + isFollowed = userData.followedTopics.contains(topic.id) + ) + }, + isSaved = userData.bookmarkedNewsResources.contains(newsResource.id) + ) +} + +fun List.mapToUserNewsResources(userData: UserData): List { + return map { UserNewsResource(it, userData) } +} + +val previewUserNewsResources = listOf( + UserNewsResource( + id = "1", + title = "Android Basics with Compose", + content = "We released the first two units of Android Basics with Compose, our first free course that teaches Android Development with Jetpack Compose to anyone; you do not need any prior programming experience other than basic computer literacy to get started. You’ll learn the fundamentals of programming in Kotlin while building Android apps using Jetpack Compose, Android’s modern toolkit that simplifies and accelerates native UI development. These two units are just the beginning; more will be coming soon. Check out Android Basics with Compose to get started on your Android development journey", + url = "https://android-developers.googleblog.com/2022/05/new-android-basics-with-compose-course.html", + headerImageUrl = "https://developer.android.com/images/hero-assets/android-basics-compose.svg", + publishDate = LocalDateTime( + year = 2022, + monthNumber = 5, + dayOfMonth = 4, + hour = 23, + minute = 0, + second = 0, + nanosecond = 0 + ).toInstant(TimeZone.UTC), + type = Codelab, + followableTopics = listOf(previewFollowableTopics[1]), + isSaved = true + ), + UserNewsResource( + id = "2", + title = "Thanks for helping us reach 1M YouTube Subscribers", + content = "Thank you everyone for following the Now in Android series and everything the " + + "Android Developers YouTube channel has to offer. During the Android Developer " + + "Summit, our YouTube channel reached 1 million subscribers! Here’s a small video to " + + "thank you all.", + url = "https://youtu.be/-fJ6poHQrjM", + headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg", + publishDate = Instant.parse("2021-11-09T00:00:00.000Z"), + type = Video, + followableTopics = listOf(previewFollowableTopics[0], previewFollowableTopics[1]), + isSaved = false + ), + UserNewsResource( + id = "3", + title = "Transformations and customisations in the Paging Library", + content = "A demonstration of different operations that can be performed " + + "with Paging. Transformations like inserting separators, when to " + + "create a new pager, and customisation options for consuming " + + "PagingData.", + url = "https://youtu.be/ZARz0pjm5YM", + headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg", + publishDate = Instant.parse("2021-11-01T00:00:00.000Z"), + type = Video, + followableTopics = listOf(previewFollowableTopics[2]), + isSaved = false + ), + UserNewsResource( + id = "4", + title = "New Jetpack Release", + content = "New Jetpack release includes updates to libraries such as CameraX, Benchmark, and" + + "more!", + url = "https://developer.android.com/jetpack/androidx/versions/all-channel", + headerImageUrl = "", + publishDate = Instant.parse("2022-10-01T00:00:00.000Z"), + type = Unknown, + followableTopics = listOf(previewFollowableTopics[2]), + isSaved = true + ) +) diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesUseCaseTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCaseTest.kt similarity index 77% rename from core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesUseCaseTest.kt rename to core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCaseTest.kt index 876811e61..fe50ecf4d 100644 --- a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetSaveableNewsResourcesUseCaseTest.kt +++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCaseTest.kt @@ -16,12 +16,13 @@ package com.google.samples.apps.nowinandroid.core.domain -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository +import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import kotlin.test.assertEquals import kotlinx.coroutines.flow.first @@ -30,7 +31,7 @@ import kotlinx.datetime.Instant import org.junit.Rule import org.junit.Test -class GetSaveableNewsResourcesUseCaseTest { +class GetUserNewsResourcesUseCaseTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @@ -38,47 +39,48 @@ class GetSaveableNewsResourcesUseCaseTest { private val newsRepository = TestNewsRepository() private val userDataRepository = TestUserDataRepository() - val useCase = GetSaveableNewsResourcesUseCase(newsRepository, userDataRepository) + val useCase = GetUserNewsResourcesUseCase(newsRepository, userDataRepository) @Test fun whenNoFilters_allNewsResourcesAreReturned() = runTest { - // Obtain the saveable news resources stream. - val saveableNewsResources = useCase() + // Obtain the user news resources stream. + val userNewsResources = useCase() - // Send some news resources and bookmarks. + // Send some news resources and user data into the data repositories. newsRepository.sendNewsResources(sampleNewsResources) - userDataRepository.setNewsResourceBookmarks( - setOf(sampleNewsResources[0].id, sampleNewsResources[2].id) + + // Construct the test user data with bookmarks and followed topics. + val userData = emptyUserData.copy( + bookmarkedNewsResources = setOf(sampleNewsResources[0].id, sampleNewsResources[2].id), + followedTopics = setOf(sampleTopic1.id) ) + userDataRepository.setUserData(userData) + // Check that the correct news resources are returned with their bookmarked state. assertEquals( - listOf( - SaveableNewsResource(sampleNewsResources[0], true), - SaveableNewsResource(sampleNewsResources[1], false), - SaveableNewsResource(sampleNewsResources[2], true) - ), - saveableNewsResources.first() + sampleNewsResources.mapToUserNewsResources(userData), + userNewsResources.first() ) } @Test fun whenFilteredByTopicId_matchingNewsResourcesAreReturned() = runTest { - // Obtain a stream of saveable news resources for the given topic id. - val saveableNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id)) + // Obtain a stream of user news resources for the given topic id. + val userNewsResources = useCase(filterTopicIds = setOf(sampleTopic1.id)) - // Send some news resources and bookmarks. + // Send test data into the repositories. newsRepository.sendNewsResources(sampleNewsResources) - userDataRepository.setNewsResourceBookmarks(setOf()) + userDataRepository.setUserData(emptyUserData) // Check that only news resources with the given topic id are returned. assertEquals( sampleNewsResources .filter { it.topics.contains(sampleTopic1) } - .map { SaveableNewsResource(it, false) }, - saveableNewsResources.first() + .mapToUserNewsResources(emptyUserData), + userNewsResources.first() ) } } diff --git a/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt new file mode 100644 index 000000000..d1ea7b569 --- /dev/null +++ b/core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/UserNewsResourceTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2023 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.core.domain + +import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource +import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.FOLLOW_SYSTEM +import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Article +import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.DEFAULT +import com.google.samples.apps.nowinandroid.core.model.data.Topic +import com.google.samples.apps.nowinandroid.core.model.data.UserData +import kotlinx.datetime.Clock +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class UserNewsResourceTest { + + /** + * Given: Some user data and news resources + * When: They are combined using `UserNewsResource.from` + * Then: The correct UserNewsResources are constructed + */ + @Test + fun userNewsResourcesAreConstructedFromNewsResourcesAndUserData() { + + val newsResource1 = NewsResource( + id = "N1", + title = "Test news title", + content = "Test news content", + url = "Test URL", + headerImageUrl = "Test image URL", + publishDate = Clock.System.now(), + type = Article, + topics = listOf( + Topic( + id = "T1", + name = "Topic 1", + shortDescription = "Topic 1 short description", + longDescription = "Topic 1 long description", + url = "Topic 1 URL", + imageUrl = "Topic 1 image URL" + ), + Topic( + id = "T2", + name = "Topic 2", + shortDescription = "Topic 2 short description", + longDescription = "Topic 2 long description", + url = "Topic 2 URL", + imageUrl = "Topic 2 image URL" + ), + ) + ) + + val userData = UserData( + bookmarkedNewsResources = setOf("N1"), + followedTopics = setOf("T1"), + themeBrand = DEFAULT, + darkThemeConfig = FOLLOW_SYSTEM, + shouldHideOnboarding = true + ) + + val userNewsResource = UserNewsResource(newsResource1, userData) + + // Check that the simple field mappings have been done correctly. + assertEquals(newsResource1.id, userNewsResource.id) + assertEquals(newsResource1.title, userNewsResource.title) + assertEquals(newsResource1.content, userNewsResource.content) + assertEquals(newsResource1.url, userNewsResource.url) + assertEquals(newsResource1.headerImageUrl, userNewsResource.headerImageUrl) + assertEquals(newsResource1.publishDate, userNewsResource.publishDate) + + // Check that each Topic has been converted to a FollowedTopic correctly. + assertEquals(newsResource1.topics.size, userNewsResource.followableTopics.size) + for (topic in newsResource1.topics) { + + // Construct the expected FollowableTopic. + val followableTopic = FollowableTopic( + topic = topic, + isFollowed = userData.followedTopics.contains(topic.id) + ) + assertTrue(userNewsResource.followableTopics.contains(followableTopic)) + } + + // Check that the saved flag is set correctly. + assertEquals( + userData.bookmarkedNewsResources.contains(newsResource1.id), + userNewsResource.isSaved + ) + } +} diff --git a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt index 30ccbd402..8dba1bfc7 100644 --- a/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt +++ b/core/testing/src/main/java/com/google/samples/apps/nowinandroid/core/testing/repository/TestUserDataRepository.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filterNotNull -private val emptyUserData = UserData( +val emptyUserData = UserData( bookmarkedNewsResources = emptySet(), followedTopics = emptySet(), themeBrand = ThemeBrand.DEFAULT, @@ -98,4 +98,11 @@ class TestUserDataRepository : UserDataRepository { */ fun getCurrentFollowedTopics(): Set? = _userData.replayCache.firstOrNull()?.followedTopics + + /** + * A test-only API to allow setting of user data directly. + */ + fun setUserData(userData: UserData) { + _userData.tryEmit(userData) + } } diff --git a/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt b/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt index 69f919a62..6fbe0b0e4 100644 --- a/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt +++ b/core/ui/src/androidTest/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardTest.kt @@ -17,10 +17,12 @@ package com.google.samples.apps.nowinandroid.core.ui import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertContentDescriptionEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText -import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources +import com.google.samples.apps.nowinandroid.core.domain.model.previewFollowableTopics +import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources import org.junit.Rule import org.junit.Test @@ -30,12 +32,12 @@ class NewsResourceCardTest { @Test fun testMetaDataDisplay_withCodelabResource() { - val newsWithKnownResourceType = previewNewsResources[0] + val newsWithKnownResourceType = previewUserNewsResources[0] var dateFormatted = "" composeTestRule.setContent { NewsResourceCardExpanded( - newsResource = newsWithKnownResourceType, + userNewsResource = newsWithKnownResourceType, isBookmarked = false, onToggleBookmark = {}, onClick = {} @@ -57,12 +59,12 @@ class NewsResourceCardTest { @Test fun testMetaDataDisplay_withUnknownResource() { - val newsWithUnknownResourceType = previewNewsResources[3] + val newsWithUnknownResourceType = previewUserNewsResources[3] var dateFormatted = "" composeTestRule.setContent { NewsResourceCardExpanded( - newsResource = newsWithUnknownResourceType, + userNewsResource = newsWithUnknownResourceType, isBookmarked = false, onToggleBookmark = {}, onClick = {} @@ -75,4 +77,23 @@ class NewsResourceCardTest { .onNodeWithText(dateFormatted) .assertIsDisplayed() } + + @Test + fun testTopicsChipColorBackground_matchesFollowedState() { + composeTestRule.setContent { + NewsResourceTopics(topics = previewFollowableTopics) + } + + for (followableTopic in previewFollowableTopics) { + val topicName = followableTopic.topic.name + val expectedContentDescription = if (followableTopic.isFollowed) { + "$topicName is followed" + } else { + "$topicName is not followed" + } + composeTestRule + .onNodeWithText(topicName.uppercase()) + .assertContentDescriptionEquals(expectedContentDescription) + } + } } diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt index fbc9c0b81..44c810a36 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsFeed.kt @@ -37,8 +37,8 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource -import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources /** * An extension on [LazyListScope] defining a feed with news resources. @@ -51,21 +51,21 @@ fun LazyGridScope.newsFeed( when (feedState) { NewsFeedUiState.Loading -> Unit is NewsFeedUiState.Success -> { - items(feedState.feed, key = { it.newsResource.id }) { saveableNewsResource -> + items(feedState.feed, key = { it.id }) { userNewsResource -> val resourceUrl by remember { - mutableStateOf(Uri.parse(saveableNewsResource.newsResource.url)) + mutableStateOf(Uri.parse(userNewsResource.url)) } val context = LocalContext.current val backgroundColor = MaterialTheme.colorScheme.background.toArgb() NewsResourceCardExpanded( - newsResource = saveableNewsResource.newsResource, - isBookmarked = saveableNewsResource.isSaved, + userNewsResource = userNewsResource, + isBookmarked = userNewsResource.isSaved, onClick = { launchCustomChromeTab(context, resourceUrl, backgroundColor) }, onToggleBookmark = { onNewsResourcesCheckedChanged( - saveableNewsResource.newsResource.id, - !saveableNewsResource.isSaved + userNewsResource.id, + !userNewsResource.isSaved ) } ) @@ -100,7 +100,7 @@ sealed interface NewsFeedUiState { /** * The list of news resources contained in this feed. */ - val feed: List + val feed: List ) : NewsFeedUiState } @@ -125,12 +125,7 @@ private fun NewsFeedContentPreview() { LazyVerticalGrid(columns = GridCells.Adaptive(300.dp)) { newsFeed( feedState = NewsFeedUiState.Success( - previewNewsResources.map { - SaveableNewsResource( - it, - false - ) - } + previewUserNewsResources ), onNewsResourcesCheckedChanged = { _, _ -> } ) diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index 96fe73752..76657067e 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview @@ -56,10 +57,11 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconT import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag 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.domain.model.FollowableTopic +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType -import com.google.samples.apps.nowinandroid.core.model.data.Topic -import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Locale @@ -73,7 +75,7 @@ import kotlinx.datetime.toJavaInstant @OptIn(ExperimentalMaterial3Api::class) @Composable fun NewsResourceCardExpanded( - newsResource: NewsResource, + userNewsResource: UserNewsResource, isBookmarked: Boolean, onToggleBookmark: () -> Unit, onClick: () -> Unit, @@ -91,9 +93,9 @@ fun NewsResourceCardExpanded( } ) { Column { - if (!newsResource.headerImageUrl.isNullOrEmpty()) { + if (!userNewsResource.headerImageUrl.isNullOrEmpty()) { Row { - NewsResourceHeaderImage(newsResource.headerImageUrl) + NewsResourceHeaderImage(userNewsResource.headerImageUrl) } } Box( @@ -103,18 +105,18 @@ fun NewsResourceCardExpanded( Spacer(modifier = Modifier.height(12.dp)) Row { NewsResourceTitle( - newsResource.title, + userNewsResource.title, modifier = Modifier.fillMaxWidth((.8f)) ) Spacer(modifier = Modifier.weight(1f)) BookmarkButton(isBookmarked, onToggleBookmark) } Spacer(modifier = Modifier.height(12.dp)) - NewsResourceMetaData(newsResource.publishDate, newsResource.type) + NewsResourceMetaData(userNewsResource.publishDate, userNewsResource.type) Spacer(modifier = Modifier.height(12.dp)) - NewsResourceShortDescription(newsResource.content) + NewsResourceShortDescription(userNewsResource.content) Spacer(modifier = Modifier.height(12.dp)) - NewsResourceTopics(newsResource.topics) + NewsResourceTopics(userNewsResource.followableTopics) } } } @@ -228,7 +230,7 @@ fun NewsResourceShortDescription( @Composable fun NewsResourceTopics( - topics: List, + topics: List, modifier: Modifier = Modifier ) { // Store the ID of the Topic which has its "following" menu expanded, if any. @@ -239,17 +241,35 @@ fun NewsResourceTopics( modifier = modifier.horizontalScroll(rememberScrollState()), // causes narrow chips horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - for (topic in topics) { + for (followableTopic in topics) { NiaTopicTag( - expanded = expandedTopicId == topic.id, - followed = true, // ToDo: Check if topic is followed + expanded = expandedTopicId == followableTopic.topic.id, + followed = followableTopic.isFollowed, onDropdownMenuToggle = { show -> - expandedTopicId = if (show) topic.id else null + expandedTopicId = if (show) followableTopic.topic.id else null }, onFollowClick = { }, // ToDo onUnfollowClick = { }, // ToDo onBrowseClick = { }, // ToDo - text = { Text(text = topic.name.uppercase(Locale.getDefault())) } + text = { + val contentDescription = if (followableTopic.isFollowed) { + stringResource( + R.string.topic_chip_content_description_when_followed, + followableTopic.topic.name + ) + } else { + stringResource( + R.string.topic_chip_content_description_when_not_followed, + followableTopic.topic.name + ) + } + Text( + text = followableTopic.topic.name.uppercase(Locale.getDefault()), + modifier = Modifier.semantics { + this.contentDescription = contentDescription + } + ) + } ) } } @@ -281,7 +301,7 @@ private fun ExpandedNewsResourcePreview() { NiaTheme { Surface { NewsResourceCardExpanded( - newsResource = previewNewsResources[0], + userNewsResource = previewUserNewsResources[0], isBookmarked = true, onToggleBookmark = {}, onClick = {} diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt index c76a8124d..e0bd22785 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCardList.kt @@ -23,42 +23,37 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext -import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource /** - * Extension function for displaying a [List] of [NewsResourceCardExpanded] backed by a generic - * [List] [T]. + * Extension function for displaying a [List] of [NewsResourceCardExpanded] backed by a list of + * [UserNewsResource]s. * - * [newsResourceMapper] maps type [T] to a [NewsResource] - * [isBookmarkedMapper] maps type [T] to whether the [NewsResource] is bookmarked * [onToggleBookmark] defines the action invoked when a user wishes to bookmark an item * [onItemClick] optional parameter for action to be performed when the card is clicked. The * default action launches an intent matching the card. */ -fun LazyListScope.newsResourceCardItems( - items: List, - newsResourceMapper: (item: T) -> NewsResource, - isBookmarkedMapper: (item: T) -> Boolean, - onToggleBookmark: (item: T) -> Unit, - onItemClick: ((item: T) -> Unit)? = null, +fun LazyListScope.userNewsResourceCardItems( + items: List, + onToggleBookmark: (item: UserNewsResource) -> Unit, + onItemClick: ((item: UserNewsResource) -> Unit)? = null, itemModifier: Modifier = Modifier, ) = items( items = items, - key = { newsResourceMapper(it).id }, - itemContent = { item -> - val newsResource = newsResourceMapper(item) - val resourceUrl = Uri.parse(newsResource.url) + key = { it.id }, + itemContent = { userNewsResource -> + val resourceUrl = Uri.parse(userNewsResource.url) val backgroundColor = MaterialTheme.colorScheme.background.toArgb() val context = LocalContext.current NewsResourceCardExpanded( - newsResource = newsResource, - isBookmarked = isBookmarkedMapper(item), - onToggleBookmark = { onToggleBookmark(item) }, + userNewsResource = userNewsResource, + isBookmarked = userNewsResource.isSaved, + onToggleBookmark = { onToggleBookmark(userNewsResource) }, onClick = { when (onItemClick) { null -> launchCustomChromeTab(context, resourceUrl, backgroundColor) - else -> onItemClick(item) + else -> onItemClick(userNewsResource) } }, modifier = itemModifier diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 6d7c98eb1..bfb1d38de 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -21,4 +21,7 @@ Open Resource Link %1$s • %2$s + + %1$s is followed + %1$s is not followed diff --git a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt index ff033033a..1f51d2bb9 100644 --- a/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt +++ b/feature/bookmarks/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksScreenTest.kt @@ -30,8 +30,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource -import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources +import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -67,8 +66,7 @@ class BookmarksScreenTest { composeTestRule.setContent { BookmarksScreen( feedState = NewsFeedUiState.Success( - previewNewsResources.take(2) - .map { SaveableNewsResource(it, true) } + previewUserNewsResources.take(2) ), removeFromBookmarks = { } ) @@ -76,7 +74,7 @@ class BookmarksScreenTest { composeTestRule .onNodeWithText( - previewNewsResources[0].title, + previewUserNewsResources[0].title, substring = true ) .assertExists() @@ -85,14 +83,14 @@ class BookmarksScreenTest { composeTestRule.onNode(hasScrollToNodeAction()) .performScrollToNode( hasText( - previewNewsResources[1].title, + previewUserNewsResources[1].title, substring = true ) ) composeTestRule .onNodeWithText( - previewNewsResources[1].title, + previewUserNewsResources[1].title, substring = true ) .assertExists() @@ -106,11 +104,10 @@ class BookmarksScreenTest { composeTestRule.setContent { BookmarksScreen( feedState = NewsFeedUiState.Success( - previewNewsResources.take(2) - .map { SaveableNewsResource(it, true) } + previewUserNewsResources.take(2) ), removeFromBookmarks = { newsResourceId -> - assertEquals(previewNewsResources[0].id, newsResourceId) + assertEquals(previewUserNewsResources[0].id, newsResourceId) removeFromBookmarksCalled = true } ) @@ -124,7 +121,7 @@ class BookmarksScreenTest { ).filter( hasAnyAncestor( hasText( - previewNewsResources[0].title, + previewUserNewsResources[0].title, substring = true ) ) 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 513c8fbd0..d5d70e9f7 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 @@ -52,8 +52,7 @@ import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource -import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources +import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources 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.Success @@ -184,9 +183,7 @@ private fun BookmarksGridPreview() { NiaTheme { BookmarksGrid( feedState = Success( - previewNewsResources.map { - SaveableNewsResource(it, false) - } + previewUserNewsResources ), removeFromBookmarks = {} ) 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 8223ecd1b..db94839be 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 @@ -19,8 +19,8 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository -import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading import dagger.hilt.android.lifecycle.HiltViewModel @@ -36,13 +36,13 @@ import kotlinx.coroutines.launch @HiltViewModel class BookmarksViewModel @Inject constructor( private val userDataRepository: UserDataRepository, - getSaveableNewsResources: GetSaveableNewsResourcesUseCase + getSaveableNewsResources: GetUserNewsResourcesUseCase ) : ViewModel() { val feedUiState: StateFlow = getSaveableNewsResources() .filterNot { it.isEmpty() } - .map { newsResources -> newsResources.filter(SaveableNewsResource::isSaved) } // Only show bookmarked news resources. - .map, NewsFeedUiState>(NewsFeedUiState::Success) + .map { newsResources -> newsResources.filter(UserNewsResource::isSaved) } // Only show bookmarked news resources. + .map, NewsFeedUiState>(NewsFeedUiState::Success) .onStart { emit(Loading) } .stateIn( scope = viewModelScope, diff --git a/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt b/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt index 56f30eff5..a8da1a4ff 100644 --- a/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt +++ b/feature/bookmarks/src/test/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModelTest.kt @@ -16,7 +16,7 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks -import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase +import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository @@ -43,7 +43,7 @@ class BookmarksViewModelTest { private val userDataRepository = TestUserDataRepository() private val newsRepository = TestNewsRepository() - private val getSaveableNewsResourcesUseCase = GetSaveableNewsResourcesUseCase( + private val getUserNewsResourcesUseCase = GetUserNewsResourcesUseCase( newsRepository = newsRepository, userDataRepository = userDataRepository ) @@ -53,7 +53,7 @@ class BookmarksViewModelTest { fun setup() { viewModel = BookmarksViewModel( userDataRepository = userDataRepository, - getSaveableNewsResources = getSaveableNewsResourcesUseCase + getSaveableNewsResources = getUserNewsResourcesUseCase ) } diff --git a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt index 702a963c1..70f60811b 100644 --- a/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt +++ b/feature/foryou/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreenTest.kt @@ -29,9 +29,8 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performScrollToNode import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources import com.google.samples.apps.nowinandroid.core.model.data.Topic -import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import org.junit.Rule import org.junit.Test @@ -225,9 +224,7 @@ class ForYouScreenTest { isSyncing = false, onboardingUiState = OnboardingUiState.NotShown, feedState = NewsFeedUiState.Success( - feed = previewNewsResources.map { - SaveableNewsResource(it, false) - } + feed = previewUserNewsResources ), onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, @@ -237,7 +234,7 @@ class ForYouScreenTest { composeTestRule .onNodeWithText( - previewNewsResources[0].title, + previewUserNewsResources[0].title, substring = true ) .assertExists() @@ -246,14 +243,14 @@ class ForYouScreenTest { composeTestRule.onNode(hasScrollToNodeAction()) .performScrollToNode( hasText( - previewNewsResources[1].title, + previewUserNewsResources[1].title, substring = true ) ) composeTestRule .onNodeWithText( - previewNewsResources[1].title, + previewUserNewsResources[1].title, substring = true ) .assertExists() diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 1427aecff..0e62a9b0b 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -83,8 +83,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaOverl 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.domain.model.FollowableTopic -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource -import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources +import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState @@ -396,9 +395,7 @@ fun ForYouScreenPopulatedFeed() { isSyncing = false, onboardingUiState = OnboardingUiState.NotShown, feedState = NewsFeedUiState.Success( - feed = previewNewsResources.map { - SaveableNewsResource(it, false) - } + feed = previewUserNewsResources ), onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, @@ -417,9 +414,7 @@ fun ForYouScreenOfflinePopulatedFeed() { isSyncing = false, onboardingUiState = OnboardingUiState.NotShown, feedState = NewsFeedUiState.Success( - feed = previewNewsResources.map { - SaveableNewsResource(it, false) - } + feed = previewUserNewsResources ), onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, @@ -440,9 +435,7 @@ fun ForYouScreenTopicSelection() { topics = previewTopics.map { FollowableTopic(it, false) }, ), feedState = NewsFeedUiState.Success( - feed = previewNewsResources.map { - SaveableNewsResource(it, false) - } + feed = previewUserNewsResources ), onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, @@ -478,9 +471,7 @@ fun ForYouScreenPopulatedAndLoading() { isSyncing = true, onboardingUiState = OnboardingUiState.Loading, feedState = NewsFeedUiState.Success( - feed = previewNewsResources.map { - SaveableNewsResource(it, false) - } + feed = previewUserNewsResources ), onTopicCheckedChanged = { _, _ -> }, saveFollowedTopics = {}, diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index 800cba0d3..6c9218a75 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -21,8 +21,8 @@ import androidx.lifecycle.viewModelScope import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase -import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -41,7 +41,7 @@ import kotlinx.coroutines.launch class ForYouViewModel @Inject constructor( syncStatusMonitor: SyncStatusMonitor, private val userDataRepository: UserDataRepository, - private val getSaveableNewsResources: GetSaveableNewsResourcesUseCase, + private val getSaveableNewsResources: GetUserNewsResourcesUseCase, getFollowableTopics: GetFollowableTopicsUseCase ) : ViewModel() { @@ -117,6 +117,6 @@ class ForYouViewModel @Inject constructor( } } -private fun Flow>.mapToFeedState(): Flow = - map, NewsFeedUiState>(NewsFeedUiState::Success) +private fun Flow>.mapToFeedState(): Flow = + map, NewsFeedUiState>(NewsFeedUiState::Success) .onStart { emit(NewsFeedUiState.Loading) } diff --git a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt index ad29aba60..4427b44fe 100644 --- a/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt +++ b/feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt @@ -17,15 +17,17 @@ package com.google.samples.apps.nowinandroid.feature.foryou import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase -import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase +import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.mapToUserNewsResources import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository +import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncStatusMonitor @@ -54,7 +56,7 @@ class ForYouViewModelTest { private val userDataRepository = TestUserDataRepository() private val topicsRepository = TestTopicsRepository() private val newsRepository = TestNewsRepository() - private val getSaveableNewsResourcesUseCase = GetSaveableNewsResourcesUseCase( + private val getUserNewsResourcesUseCase = GetUserNewsResourcesUseCase( newsRepository = newsRepository, userDataRepository = userDataRepository ) @@ -69,7 +71,7 @@ class ForYouViewModelTest { viewModel = ForYouViewModel( syncStatusMonitor = syncStatusMonitor, userDataRepository = userDataRepository, - getSaveableNewsResources = getSaveableNewsResourcesUseCase, + getSaveableNewsResources = getUserNewsResourcesUseCase, getFollowableTopics = getFollowableTopicsUseCase ) } @@ -263,7 +265,10 @@ class ForYouViewModelTest { val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } topicsRepository.sendTopics(sampleTopics) - userDataRepository.setFollowedTopicIds(setOf("0", "1")) + + val followedTopicIds = setOf("0", "1") + val userData = emptyUserData.copy(followedTopics = followedTopicIds) + userDataRepository.setUserData(userData) viewModel.dismissOnboarding() assertEquals( @@ -280,13 +285,7 @@ class ForYouViewModelTest { ) assertEquals( NewsFeedUiState.Success( - feed = - sampleNewsResources.map { - SaveableNewsResource( - newsResource = it, - isSaved = false - ) - } + feed = sampleNewsResources.mapToUserNewsResources(userData) ), viewModel.feedState.value ) @@ -307,41 +306,9 @@ class ForYouViewModelTest { assertEquals( OnboardingUiState.Shown( - topics = listOf( - FollowableTopic( - topic = Topic( - id = "0", - name = "Headlines", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ), - isFollowed = false - ), - FollowableTopic( - topic = Topic( - id = "1", - name = "UI", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ), - isFollowed = false - ), - FollowableTopic( - topic = Topic( - id = "2", - name = "Tools", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ), - isFollowed = false - ) - ), + topics = sampleTopics.map { + FollowableTopic(it, false) + } ), viewModel.onboardingUiState.value ) @@ -352,59 +319,25 @@ class ForYouViewModelTest { viewModel.feedState.value ) - viewModel.updateTopicSelection("1", isChecked = true) + val followedTopicId = sampleTopics[1].id + viewModel.updateTopicSelection(followedTopicId, isChecked = true) assertEquals( OnboardingUiState.Shown( - topics = listOf( - FollowableTopic( - topic = Topic( - id = "0", - name = "Headlines", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ), - isFollowed = false - ), - FollowableTopic( - topic = Topic( - id = "1", - name = "UI", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ), - isFollowed = true - ), - FollowableTopic( - topic = Topic( - id = "2", - name = "Tools", - shortDescription = "", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ), - isFollowed = false - ) - ), + topics = sampleTopics.map { + FollowableTopic(it, it.id == followedTopicId) + } ), viewModel.onboardingUiState.value ) + + val userData = emptyUserData.copy(followedTopics = setOf(followedTopicId)) + assertEquals( NewsFeedUiState.Success( feed = listOf( - SaveableNewsResource( - newsResource = sampleNewsResources[1], - isSaved = false - ), - SaveableNewsResource( - newsResource = sampleNewsResources[2], - isSaved = false - ) + UserNewsResource(sampleNewsResources[1], userData), + UserNewsResource(sampleNewsResources[2], userData), ) ), viewModel.feedState.value @@ -484,11 +417,25 @@ class ForYouViewModelTest { launch(UnconfinedTestDispatcher()) { viewModel.onboardingUiState.collect() } val collectJob2 = launch(UnconfinedTestDispatcher()) { viewModel.feedState.collect() } + val followedTopicIds = setOf("1") + val userData = emptyUserData.copy( + followedTopics = followedTopicIds, + shouldHideOnboarding = true + ) + topicsRepository.sendTopics(sampleTopics) - userDataRepository.setFollowedTopicIds(setOf("1")) - userDataRepository.setShouldHideOnboarding(true) + userDataRepository.setUserData(userData) newsRepository.sendNewsResources(sampleNewsResources) - viewModel.updateNewsResourceSaved("2", true) + + val bookmarkedNewsResourceId = "2" + viewModel.updateNewsResourceSaved( + newsResourceId = bookmarkedNewsResourceId, + isChecked = true + ) + + val userDataExpected = userData.copy( + bookmarkedNewsResources = setOf(bookmarkedNewsResourceId) + ) assertEquals( OnboardingUiState.NotShown, @@ -497,14 +444,8 @@ class ForYouViewModelTest { assertEquals( NewsFeedUiState.Success( feed = listOf( - SaveableNewsResource( - newsResource = sampleNewsResources[1], - isSaved = true - ), - SaveableNewsResource( - newsResource = sampleNewsResources[2], - isSaved = false - ) + UserNewsResource(newsResource = sampleNewsResources[1], userDataExpected), + UserNewsResource(newsResource = sampleNewsResources[2], userDataExpected) ) ), viewModel.feedState.value diff --git a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt index 9bef39a36..df9e83315 100644 --- a/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt +++ b/feature/topic/src/androidTest/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreenTest.kt @@ -25,10 +25,11 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performScrollToNode import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.Topic +import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData import kotlinx.datetime.Instant import org.junit.Before import org.junit.Rule @@ -99,14 +100,7 @@ class TopicScreenTest { composeTestRule.setContent { TopicScreen( topicUiState = TopicUiState.Loading, - newsUiState = NewsUiState.Success( - sampleNewsResources.mapIndexed { index, newsResource -> - SaveableNewsResource( - newsResource = newsResource, - isSaved = index % 2 == 0, - ) - } - ), + newsUiState = NewsUiState.Success(sampleUserNewsResources), onBackClick = { }, onFollowClick = { }, onBookmarkChanged = { _, _ -> }, @@ -126,12 +120,7 @@ class TopicScreenTest { TopicScreen( topicUiState = TopicUiState.Success(testTopic), newsUiState = NewsUiState.Success( - sampleNewsResources.mapIndexed { index, newsResource -> - SaveableNewsResource( - newsResource = newsResource, - isSaved = index % 2 == 0, - ) - } + sampleUserNewsResources ), onBackClick = { }, onFollowClick = { }, @@ -143,7 +132,7 @@ class TopicScreenTest { composeTestRule .onAllNodes(hasScrollToNodeAction()) .onFirst() - .performScrollToNode(hasText(sampleNewsResources.first().title)) + .performScrollToNode(hasText(sampleUserNewsResources.first().title)) } } @@ -188,27 +177,31 @@ private val testTopics = listOf( ) ) -private val sampleNewsResources = listOf( - NewsResource( - id = "1", - title = "Thanks for helping us reach 1M YouTube Subscribers", - content = "Thank you everyone for following the Now in Android series and everything the " + - "Android Developers YouTube channel has to offer. During the Android Developer " + - "Summit, our YouTube channel reached 1 million subscribers! Here’s a small video to " + - "thank you all.", - url = "https://youtu.be/-fJ6poHQrjM", - headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg", - publishDate = Instant.parse("2021-11-09T00:00:00.000Z"), - type = Video, - topics = listOf( - Topic( - id = "0", - name = "Headlines", - shortDescription = "", - longDescription = TOPIC_DESC, - url = "", - imageUrl = "" +private val sampleUserNewsResources = listOf( + UserNewsResource( + newsResource = + NewsResource( + id = "1", + title = "Thanks for helping us reach 1M YouTube Subscribers", + content = "Thank you everyone for following the Now in Android series and" + + " everything the Android Developers YouTube channel has to offer. During the " + + "Android Developer Summit, our YouTube channel reached 1 million subscribers!" + + " Here’s a small video to thank you all.", + url = "https://youtu.be/-fJ6poHQrjM", + headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg", + publishDate = Instant.parse("2021-11-09T00:00:00.000Z"), + type = Video, + topics = listOf( + Topic( + id = "0", + name = "Headlines", + shortDescription = "", + longDescription = TOPIC_DESC, + url = "", + imageUrl = "" + ) ) - ) + ), + userData = emptyUserData.copy(bookmarkedNewsResources = setOf("1")) ) ) diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index 61bd13aa0..b3263839f 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -53,12 +53,11 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadi 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.domain.model.FollowableTopic -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource -import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources +import com.google.samples.apps.nowinandroid.core.domain.model.previewUserNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank -import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems +import com.google.samples.apps.nowinandroid.core.ui.userNewsResourceCardItems import com.google.samples.apps.nowinandroid.feature.topic.R.string import com.google.samples.apps.nowinandroid.feature.topic.TopicUiState.Loading @@ -146,7 +145,7 @@ private fun LazyListScope.TopicBody( TopicHeader(name, description, imageUrl) } - TopicCards(news, onBookmarkChanged) + userNewsResourceCards(news, onBookmarkChanged) } @Composable @@ -174,17 +173,16 @@ private fun TopicHeader(name: String, description: String, imageUrl: String) { } } -private fun LazyListScope.TopicCards( +// TODO: Could/should this be replaced with [LazyGridScope.newsFeed]? +private fun LazyListScope.userNewsResourceCards( news: NewsUiState, onBookmarkChanged: (String, Boolean) -> Unit ) { when (news) { is NewsUiState.Success -> { - newsResourceCardItems( + userNewsResourceCardItems( items = news.news, - newsResourceMapper = { it.newsResource }, - isBookmarkedMapper = { it.isSaved }, - onToggleBookmark = { onBookmarkChanged(it.newsResource.id, !it.isSaved) }, + onToggleBookmark = { onBookmarkChanged(it.id, !it.isSaved) }, itemModifier = Modifier.padding(24.dp) ) } @@ -257,12 +255,7 @@ fun TopicScreenPopulated() { TopicScreen( topicUiState = TopicUiState.Success(FollowableTopic(previewTopics[0], false)), newsUiState = NewsUiState.Success( - previewNewsResources.mapIndexed { index, newsResource -> - SaveableNewsResource( - newsResource = newsResource, - isSaved = index % 2 == 0, - ) - } + previewUserNewsResources ), onBackClick = {}, onFollowClick = {}, diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt index 2c5f76f63..260cecbd1 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt @@ -22,9 +22,9 @@ import androidx.lifecycle.viewModelScope import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.decoder.StringDecoder -import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase +import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic -import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource +import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.asResult @@ -45,7 +45,7 @@ class TopicViewModel @Inject constructor( stringDecoder: StringDecoder, private val userDataRepository: UserDataRepository, topicsRepository: TopicsRepository, - getSaveableNewsResources: GetSaveableNewsResourcesUseCase + getSaveableNewsResources: GetUserNewsResourcesUseCase ) : ViewModel() { private val topicArgs: TopicArgs = TopicArgs(savedStateHandle, stringDecoder) @@ -130,11 +130,11 @@ private fun topicUiState( private fun newsUiState( topicId: String, - getSaveableNewsResources: GetSaveableNewsResourcesUseCase, + getSaveableNewsResources: GetUserNewsResourcesUseCase, userDataRepository: UserDataRepository, ): Flow { // Observe news - val newsStream: Flow> = getSaveableNewsResources( + val newsStream: Flow> = getSaveableNewsResources( filterTopicIds = setOf(element = topicId), ) @@ -171,7 +171,7 @@ sealed interface TopicUiState { } sealed interface NewsUiState { - data class Success(val news: List) : NewsUiState + data class Success(val news: List) : NewsUiState object Error : NewsUiState object Loading : NewsUiState } diff --git a/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt b/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt index 32768c4d7..6c61e59d5 100644 --- a/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt +++ b/feature/topic/src/test/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModelTest.kt @@ -17,7 +17,7 @@ package com.google.samples.apps.nowinandroid.feature.topic import androidx.lifecycle.SavedStateHandle -import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResourcesUseCase +import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video @@ -53,7 +53,7 @@ class TopicViewModelTest { private val userDataRepository = TestUserDataRepository() private val topicsRepository = TestTopicsRepository() private val newsRepository = TestNewsRepository() - private val getSaveableNewsResourcesUseCase = GetSaveableNewsResourcesUseCase( + private val getUserNewsResourcesUseCase = GetUserNewsResourcesUseCase( newsRepository = newsRepository, userDataRepository = userDataRepository ) @@ -66,7 +66,7 @@ class TopicViewModelTest { stringDecoder = FakeStringDecoder(), userDataRepository = userDataRepository, topicsRepository = topicsRepository, - getSaveableNewsResources = getSaveableNewsResourcesUseCase + getSaveableNewsResources = getUserNewsResourcesUseCase ) }