diff --git a/app/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt b/app/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt index d9989d2ce..84400bb09 100644 --- a/app/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt +++ b/app/src/androidInstrumentedTest/kotlin/com/google/samples/apps/nowinandroid/ui/NavigationTest.kt @@ -16,13 +16,16 @@ package com.google.samples.apps.nowinandroid.ui +import androidx.compose.ui.semantics.SemanticsActions.ScrollBy import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.assertIsSelected import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText @@ -78,6 +81,9 @@ class NavigationTest { @Inject lateinit var topicsRepository: TopicsRepository + @Inject + lateinit var newsRepository: NewsRepository + // The strings used for matching in these tests private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_navigate_up) private val forYou by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_title) @@ -270,4 +276,44 @@ class NavigationTest { onNodeWithTag("topic:${topic.id}").assertExists() } } + + @Test + fun navigatingToTopicFromForYou_showsTopicDetails() { + composeTestRule.apply { + // Get the first news resource + val newsResource = runBlocking { + newsRepository.getNewsResources().first().first() + } + + // Get its first topic and follow it + val topic = newsResource.topics.first() + onNodeWithText(topic.name).performClick() + + // Get the news feed and scroll to the news resource + // Note: Possible flakiness. If the content of the news resource is long then the topic + // tag might not be visible meaning it cannot be clicked + onNodeWithTag("forYou:feed") + .performScrollToNode(hasTestTag("newsResourceCard:${newsResource.id}")) + .fetchSemanticsNode() + .apply { + val newsResourceCardNode = onNodeWithTag("newsResourceCard:${newsResource.id}") + .fetchSemanticsNode() + config[ScrollBy].action?.invoke( + 0f, + // to ensure the bottom of the card is visible, + // manually scroll the difference between the height of + // the scrolling node and the height of the card + (newsResourceCardNode.size.height - size.height).coerceAtLeast(0).toFloat(), + ) + } + + // Click the first topic tag + onAllNodesWithTag("topicTag:${topic.id}", useUnmergedTree = true) + .onFirst() + .performClick() + + // Verify that we're on the correct topic details screen + onNodeWithTag("topic:${topic.id}").assertExists() + } + } } diff --git a/app/src/androidMain/res/values/colors.xml b/app/src/androidMain/res/values/colors.xml index 5a3dc450f..2b8c739cd 100644 --- a/app/src/androidMain/res/values/colors.xml +++ b/app/src/androidMain/res/values/colors.xml @@ -15,9 +15,6 @@ limitations under the License. --> - - #4D000000 - #000000 #FCFCFC diff --git a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt index 035c19faf..20d60e3af 100644 --- a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt +++ b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/NiaNavHost.kt @@ -20,10 +20,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksScreen -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute -import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen +import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute +import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouSection import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterests import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen +import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic +import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS import com.google.samples.apps.nowinandroid.ui.NiaAppState import com.google.samples.apps.nowinandroid.ui.interests2pane.interestsListDetailScreen @@ -44,10 +46,18 @@ fun NiaNavHost( val navController = appState.navController NavHost( navController = navController, - startDestination = ForYouRoute, + startDestination = ForYouBaseRoute, modifier = modifier, ) { - forYouScreen(onTopicClick = navController::navigateToInterests) + forYouSection( + onTopicClick = navController::navigateToTopic, + ) { + topicScreen( + showBackButton = true, + onBackClick = navController::popBackStack, + onTopicClick = navController::navigateToTopic, + ) + } bookmarksScreen( onTopicClick = navController::navigateToInterests, onShowSnackbar = onShowSnackbar, diff --git a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt index d6bf34d4e..32592eb6c 100644 --- a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt +++ b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/navigation/TopLevelDestination.kt @@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid.navigation import androidx.compose.ui.graphics.vector.ImageVector import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.BookmarksRoute +import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouBaseRoute import com.google.samples.apps.nowinandroid.feature.foryou.navigation.ForYouRoute import com.google.samples.apps.nowinandroid.feature.interests.navigation.InterestsRoute import nowinandroid.feature.bookmarks.generated.resources.feature_bookmarks_title @@ -33,9 +34,18 @@ import nowinandroid.feature.foryou.generated.resources.Res as forYouR import nowinandroid.feature.search.generated.resources.Res as searchR /** - * Type for the top level destinations in the application. Each of these destinations - * can contain one or more screens (based on the window size). Navigation from one screen to the - * next within a single destination will be handled directly in composables. + * Type for the top level destinations in the application. Contains metadata about the destination + * that is used in the top app bar and common navigation UI. + * + * @param selectedIcon The icon to be displayed in the navigation UI when this destination is + * selected. + * @param unselectedIcon The icon to be displayed in the navigation UI when this destination is + * not selected. + * @param iconTextId Text that to be displayed in the navigation UI. + * @param titleTextId Text that is displayed on the top app bar. + * @param route The route to use when navigating to this destination. + * @param baseRoute The highest ancestor of this destination. Defaults to [route], meaning that + * there is a single destination in that section of the app (no nested destinations). */ enum class TopLevelDestination( val selectedIcon: ImageVector, @@ -43,6 +53,7 @@ enum class TopLevelDestination( val iconTextId: StringResource, val titleTextId: StringResource, val route: KClass<*>, + val baseRoute: KClass<*> = route, ) { FOR_YOU( selectedIcon = NiaIcons.Upcoming, @@ -50,6 +61,7 @@ enum class TopLevelDestination( iconTextId = forYouR.string.feature_foryou_title, titleTextId = Res.string.app_name, route = ForYouRoute::class, + baseRoute = ForYouBaseRoute::class, ), BOOKMARKS( selectedIcon = NiaIcons.Bookmarks, diff --git a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt index 492961df1..143e6e1ff 100644 --- a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt +++ b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaApp.kt @@ -147,7 +147,7 @@ internal fun NiaApp( appState.topLevelDestinations.forEach { destination -> val hasUnread = unreadDestinations.contains(destination) val selected = currentDestination - .isRouteInHierarchy(destination.route) + .isRouteInHierarchy(destination.baseRoute) item( selected = selected, onClick = { appState.navigateToTopLevelDestination(destination) }, diff --git a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt index 3b1ed4a52..7647d4f33 100644 --- a/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt +++ b/app/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt @@ -88,7 +88,7 @@ class NiaAppState( val currentTopLevelDestination: TopLevelDestination? @Composable get() { return TopLevelDestination.entries.firstOrNull { topLevelDestination -> - currentDestination?.hasRoute(route = topLevelDestination.route) ?: false + currentDestination?.hasRoute(route = topLevelDestination.route) == true } } diff --git a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png index e052b5920..30873b584 100644 Binary files a/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png and b/app/src/testDemo/screenshots/compactWidth_expandedHeight_showsNavigationBar.png differ diff --git a/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png index 53bf6f3c5..e2dffaf01 100644 Binary files a/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/expandedWidth_expandedHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png index 3d2c79256..253b6be4c 100644 Binary files a/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png and b/app/src/testDemo/screenshots/insets_snackbar_expanded_expanded.png differ diff --git a/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png b/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png index 3e38938d6..79f808f44 100644 Binary files a/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png and b/app/src/testDemo/screenshots/mediumWidth_expandedHeight_showsNavigationRail.png differ diff --git a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png index 4997a83af..841a02eaf 100644 Binary files a/app/src/testDemo/screenshots/snackbar_expanded_expanded.png and b/app/src/testDemo/screenshots/snackbar_expanded_expanded.png differ diff --git a/app/src/testDemo/screenshots/snackbar_medium_medium.png b/app/src/testDemo/screenshots/snackbar_medium_medium.png index 36fffa9c6..2800575b8 100644 Binary files a/app/src/testDemo/screenshots/snackbar_medium_medium.png and b/app/src/testDemo/screenshots/snackbar_medium_medium.png differ diff --git a/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt index 35932c835..652409db6 100644 --- a/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt @@ -17,6 +17,8 @@ import com.google.samples.apps.nowinandroid.configureKotlinJvm import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.kotlin class JvmLibraryConventionPlugin : Plugin { override fun apply(target: Project) { @@ -26,6 +28,9 @@ class JvmLibraryConventionPlugin : Plugin { apply("nowinandroid.android.lint") } configureKotlinJvm() + dependencies { + add("testImplementation", kotlin("test")) + } } } } diff --git a/core/data-test/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt b/core/data-test/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt index e4a2ea6b9..5767c9242 100644 --- a/core/data-test/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt +++ b/core/data-test/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/test/repository/FakeNewsRepository.kt @@ -17,14 +17,11 @@ package com.google.samples.apps.nowinandroid.core.data.test.repository import com.google.samples.apps.nowinandroid.core.data.Synchronizer -import com.google.samples.apps.nowinandroid.core.data.model.asEntity +import com.google.samples.apps.nowinandroid.core.data.model.asExternalModel import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery -import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity -import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.network.demo.DemoNiaNetworkDataSource -import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -45,9 +42,11 @@ class FakeNewsRepository( query: NewsResourceQuery, ): Flow> = flow { + val newsResources = datasource.getNewsResources() + val topics = datasource.getTopics() + emit( - datasource - .getNewsResources() + newsResources .filter { networkNewsResource -> // Filter out any news resources which don't match the current query. // If no query parameters (filterTopicIds or filterNewsIds) are specified @@ -61,8 +60,7 @@ class FakeNewsRepository( ) .all(true::equals) } - .map(NetworkNewsResource::asEntity) - .map(NewsResourceEntity::asExternalModel), + .map { it.asExternalModel(topics) }, ) }.flowOn(ioDispatcher) diff --git a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NewsResource.kt b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NewsResource.kt index c3ad91dfe..01d0905f0 100644 --- a/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NewsResource.kt +++ b/core/data/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NewsResource.kt @@ -19,8 +19,10 @@ package com.google.samples.apps.nowinandroid.core.data.model import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity +import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource -import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded +import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic +import com.google.samples.apps.nowinandroid.core.network.model.asExternalModel fun NetworkNewsResource.asEntity() = NewsResourceEntity( id = id, @@ -32,16 +34,6 @@ fun NetworkNewsResource.asEntity() = NewsResourceEntity( type = type, ) -fun NetworkNewsResourceExpanded.asEntity() = NewsResourceEntity( - id = id, - title = title, - content = content, - url = url, - headerImageUrl = headerImageUrl, - publishDate = publishDate, - type = type, -) - /** * A shell [TopicEntity] to fulfill the foreign key constraint when inserting * a [NewsResourceEntity] into the DB @@ -65,3 +57,17 @@ fun NetworkNewsResource.topicCrossReferences(): List topicId = topicId, ) } + +fun NetworkNewsResource.asExternalModel(topics: List) = + NewsResource( + id = id, + title = title, + content = content, + url = url, + headerImageUrl = headerImageUrl, + publishDate = publishDate, + type = type, + topics = topics + .filter { networkTopic -> this.topics.contains(networkTopic.id) } + .map(NetworkTopic::asExternalModel), + ) diff --git a/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityKtTest.kt b/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityKtTest.kt deleted file mode 100644 index da64b9ee8..000000000 --- a/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityKtTest.kt +++ /dev/null @@ -1,91 +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.data.model - -import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource -import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded -import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic -import kotlinx.datetime.Instant -import kotlin.test.Test -import kotlin.test.assertEquals - -class NetworkEntityKtTest { - - @Test - fun network_topic_can_be_mapped_to_topic_entity() { - val networkModel = NetworkTopic( - id = "0", - name = "Test", - shortDescription = "short description", - longDescription = "long description", - url = "URL", - imageUrl = "image URL", - ) - val entity = networkModel.asEntity() - - assertEquals("0", entity.id) - assertEquals("Test", entity.name) - assertEquals("short description", entity.shortDescription) - assertEquals("long description", entity.longDescription) - assertEquals("URL", entity.url) - assertEquals("image URL", entity.imageUrl) - } - - @Test - fun network_news_resource_can_be_mapped_to_news_resource_entity() { - val networkModel = - NetworkNewsResource( - id = "0", - title = "title", - content = "content", - url = "url", - headerImageUrl = "headerImageUrl", - publishDate = Instant.fromEpochMilliseconds(1), - type = "Article 📚", - ) - val entity = networkModel.asEntity() - - assertEquals("0", entity.id) - assertEquals("title", entity.title) - assertEquals("content", entity.content) - assertEquals("url", entity.url) - assertEquals("headerImageUrl", entity.headerImageUrl) - assertEquals(Instant.fromEpochMilliseconds(1), entity.publishDate) - assertEquals("Article 📚", entity.type) - - val expandedNetworkModel = - NetworkNewsResourceExpanded( - id = "0", - title = "title", - content = "content", - url = "url", - headerImageUrl = "headerImageUrl", - publishDate = Instant.fromEpochMilliseconds(1), - type = "Article 📚", - ) - - val entityFromExpanded = expandedNetworkModel.asEntity() - - assertEquals("0", entityFromExpanded.id) - assertEquals("title", entityFromExpanded.title) - assertEquals("content", entityFromExpanded.content) - assertEquals("url", entityFromExpanded.url) - assertEquals("headerImageUrl", entityFromExpanded.headerImageUrl) - assertEquals(Instant.fromEpochMilliseconds(1), entityFromExpanded.publishDate) - assertEquals("Article 📚", entityFromExpanded.type) - } -} diff --git a/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityTest.kt b/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityTest.kt new file mode 100644 index 000000000..52dbe5117 --- /dev/null +++ b/core/data/src/commonTest/kotlin/com/google/samples/apps/nowinandroid/core/data/model/NetworkEntityTest.kt @@ -0,0 +1,140 @@ +/* + * 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.data.model + +import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import com.google.samples.apps.nowinandroid.core.model.data.Topic +import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource +import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic +import com.google.samples.apps.nowinandroid.core.network.model.asExternalModel +import kotlinx.datetime.Instant +import org.junit.Test +import kotlin.test.assertEquals + +class NetworkEntityTest { + + @Test + fun networkTopicMapsToDatabaseModel() { + val networkModel = NetworkTopic( + id = "0", + name = "Test", + shortDescription = "short description", + longDescription = "long description", + url = "URL", + imageUrl = "image URL", + ) + val entity = networkModel.asEntity() + + assertEquals("0", entity.id) + assertEquals("Test", entity.name) + assertEquals("short description", entity.shortDescription) + assertEquals("long description", entity.longDescription) + assertEquals("URL", entity.url) + assertEquals("image URL", entity.imageUrl) + } + + @Test + fun networkNewsResourceMapsToDatabaseModel() { + val networkModel = + NetworkNewsResource( + id = "0", + title = "title", + content = "content", + url = "url", + headerImageUrl = "headerImageUrl", + publishDate = Instant.fromEpochMilliseconds(1), + type = "Article 📚", + ) + val entity = networkModel.asEntity() + + assertEquals("0", entity.id) + assertEquals("title", entity.title) + assertEquals("content", entity.content) + assertEquals("url", entity.url) + assertEquals("headerImageUrl", entity.headerImageUrl) + assertEquals(Instant.fromEpochMilliseconds(1), entity.publishDate) + assertEquals("Article 📚", entity.type) + } + + @Test + fun networkTopicMapsToExternalModel() { + val networkTopic = NetworkTopic( + id = "0", + name = "Test", + shortDescription = "short description", + longDescription = "long description", + url = "URL", + imageUrl = "imageUrl", + ) + + val expected = Topic( + id = "0", + name = "Test", + shortDescription = "short description", + longDescription = "long description", + url = "URL", + imageUrl = "imageUrl", + ) + + assertEquals(expected, networkTopic.asExternalModel()) + } + + @Test + fun networkNewsResourceMapsToExternalModel() { + val networkNewsResource = NetworkNewsResource( + id = "0", + title = "title", + content = "content", + url = "url", + headerImageUrl = "headerImageUrl", + publishDate = Instant.fromEpochMilliseconds(1), + type = "Article 📚", + topics = listOf("1", "2"), + ) + + val networkTopics = listOf( + NetworkTopic( + id = "1", + name = "Test 1", + shortDescription = "short description 1", + longDescription = "long description 1", + url = "url 1", + imageUrl = "imageUrl 1", + ), + NetworkTopic( + id = "2", + name = "Test 2", + shortDescription = "short description 2", + longDescription = "long description 2", + url = "url 2", + imageUrl = "imageUrl 2", + ), + ) + + val expected = NewsResource( + id = "0", + title = "title", + content = "content", + url = "url", + headerImageUrl = "headerImageUrl", + publishDate = Instant.fromEpochMilliseconds(1), + type = "Article 📚", + topics = networkTopics.map(NetworkTopic::asExternalModel), + ) + assertEquals(expected, networkNewsResource.asExternalModel(networkTopics)) + } +} diff --git a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkNewsResource.kt b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkNewsResource.kt index 89af19c99..7b66af796 100644 --- a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkNewsResource.kt +++ b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkNewsResource.kt @@ -34,18 +34,3 @@ data class NetworkNewsResource( val type: String, val topics: List = listOf(), ) - -/** - * Network representation of [NewsResource] when fetched from /newsresources/{id} - */ -@Serializable -data class NetworkNewsResourceExpanded( - val id: String, - val title: String, - val content: String, - val url: String, - val headerImageUrl: String, - val publishDate: Instant, - val type: String, - val topics: List = listOf(), -) diff --git a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkTopic.kt b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkTopic.kt index e1043938f..0d21c09e7 100644 --- a/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkTopic.kt +++ b/core/network/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/network/model/NetworkTopic.kt @@ -32,3 +32,13 @@ data class NetworkTopic( val imageUrl: String = "", val followed: Boolean = false, ) + +fun NetworkTopic.asExternalModel(): Topic = + Topic( + id = id, + name = name, + shortDescription = shortDescription, + longDescription = longDescription, + url = url, + imageUrl = imageUrl, + ) diff --git a/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt b/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt index 5e2b25abd..9f7fd176e 100644 --- a/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt +++ b/core/ui/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/core/ui/NewsResourceCard.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics @@ -114,9 +115,11 @@ fun NewsResourceCardExpanded( colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), // Use custom label for accessibility services to communicate button's action to user. // Pass null for action to only override the label and not the actual action. - modifier = modifier.semantics { - onClick(label = clickActionLabel, action = null) - }, + modifier = modifier + .semantics { + onClick(label = clickActionLabel, action = null) + } + .testTag("newsResourceCard:${userNewsResource.id}"), ) { Column { if (!userNewsResource.headerImageUrl.isNullOrEmpty()) { @@ -334,9 +337,11 @@ fun NewsResourceTopics( } Text( text = followableTopic.topic.name.uppercase(), - modifier = Modifier.semantics { - this.contentDescription = contentDescription - }, + modifier = Modifier + .semantics { + this.contentDescription = contentDescription + } + .testTag("topicTag:${followableTopic.topic.id}"), ) }, ) diff --git a/feature/foryou/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt b/feature/foryou/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt index 8898c471a..f78911397 100644 --- a/feature/foryou/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt +++ b/feature/foryou/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/foryou/navigation/ForYouNavigation.kt @@ -20,32 +20,47 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation import androidx.navigation.navDeepLink import com.google.samples.apps.nowinandroid.core.notifications.DEEP_LINK_URI_PATTERN import com.google.samples.apps.nowinandroid.feature.foryou.ForYouScreen import kotlinx.serialization.Serializable -@Serializable -data object ForYouRoute +@Serializable data object ForYouRoute // route to ForYou screen + +@Serializable data object ForYouBaseRoute // route to base navigation graph fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(route = ForYouRoute, navOptions) -fun NavGraphBuilder.forYouScreen(onTopicClick: (String) -> Unit) { - composable( - deepLinks = listOf( - navDeepLink { - /** - * This destination has a deep link that enables a specific news resource to be - * opened from a notification (@see SystemTrayNotifier for more). The news resource - * ID is sent in the URI rather than being modelled in the route type because it's - * transient data (stored in SavedStateHandle) that is cleared after the user has - * opened the news resource. - */ - uriPattern = DEEP_LINK_URI_PATTERN - }, - ), - ) { - ForYouScreen(onTopicClick) +/** + * The ForYou section of the app. It can also display information about topics. + * This should be supplied from a separate module. + * + * @param onTopicClick - Called when a topic is clicked, contains the ID of the topic + * @param topicDestination - Destination for topic content + */ +fun NavGraphBuilder.forYouSection( + onTopicClick: (String) -> Unit, + topicDestination: NavGraphBuilder.() -> Unit, +) { + navigation(startDestination = ForYouRoute) { + composable( + deepLinks = listOf( + navDeepLink { + /** + * This destination has a deep link that enables a specific news resource to be + * opened from a notification (@see SystemTrayNotifier for more). The news resource + * ID is sent in the URI rather than being modelled in the route type because it's + * transient data (stored in SavedStateHandle) that is cleared after the user has + * opened the news resource. + */ + uriPattern = DEEP_LINK_URI_PATTERN + }, + ), + ) { + ForYouScreen(onTopicClick) + } + topicDestination() } } diff --git a/feature/topic/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index be8808977..2caa41967 100644 --- a/feature/topic/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/commonMain/kotlin/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -74,7 +74,7 @@ import org.jetbrains.compose.ui.tooling.preview.PreviewParameter import org.koin.compose.viewmodel.koinViewModel @Composable -internal fun TopicScreen( +fun TopicScreen( showBackButton: Boolean, onBackClick: () -> Unit, onTopicClick: (String) -> Unit, diff --git a/lint/build.gradle.kts b/lint/build.gradle.kts index cfda9d64d..99a057362 100644 --- a/lint/build.gradle.kts +++ b/lint/build.gradle.kts @@ -15,7 +15,6 @@ */ import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` diff --git a/settings.gradle.kts b/settings.gradle.kts index c7c765d80..b11358e53 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -65,3 +65,11 @@ include(":lint") include(":shared") include(":sync:work") include(":sync:sync-test") + +check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { + """ + Now in Android requires JDK 17+ but it is currently using JDK ${JavaVersion.current()}. + Java Home: [${System.getProperty("java.home")}] + https://developer.android.com/build/jdks#jdk-config-in-studio + """.trimIndent() +}