Skip to content

Commit

Permalink
Display unread state on the news feed and bottom nav bar
Browse files Browse the repository at this point in the history
When a news resource is unread, display a dot on its card in the news
feed.  When the For You section has unread resources, display a dot on
its icon in the navigation bar.

Update the read status when a resource is opened.
  • Loading branch information
rosejr committed Feb 14, 2023
1 parent bc82f43 commit 0a5d076
Show file tree
Hide file tree
Showing 14 changed files with 134 additions and 4 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ dependencies {
implementation(project(":core:ui"))
implementation(project(":core:designsystem"))
implementation(project(":core:data"))
implementation(project(":core:domain"))
implementation(project(":core:model"))
implementation(project(":core:analytics"))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.ui.NiaApp
Expand All @@ -67,6 +68,9 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var analyticsHelper: AnalyticsHelper

@Inject
lateinit var userNewsResourceRepository: UserNewsResourceRepository

val viewModel: MainActivityViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -119,6 +123,7 @@ class MainActivity : ComponentActivity() {
NiaApp(
networkMonitor = networkMonitor,
windowSizeClass = calculateWindowSizeClass(this),
userNewsResourceRepository = userNewsResourceRepository,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination
Expand All @@ -68,6 +71,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.icon.Icon.ImageVec
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
import com.google.samples.apps.nowinandroid.core.domain.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
Expand All @@ -87,6 +91,7 @@ fun NiaApp(
networkMonitor = networkMonitor,
windowSizeClass = windowSizeClass,
),
userNewsResourceRepository: UserNewsResourceRepository,
) {
val shouldShowGradientBackground =
appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
Expand Down Expand Up @@ -130,8 +135,17 @@ fun NiaApp(
snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = {
if (appState.shouldShowBottomBar) {
val forYouNewsResources by userNewsResourceRepository.getUserNewsResourcesForFollowedTopics()
.collectAsStateWithLifecycle(emptyList())
val unreadDestinations =
when {
forYouNewsResources.all { it.isViewed } -> emptySet()
else -> setOf(TopLevelDestination.FOR_YOU)
}

NiaBottomBar(
destinations = appState.topLevelDestinations,
destinationsWithUnreadResources = unreadDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination,
modifier = Modifier.testTag("NiaBottomBar"),
Expand Down Expand Up @@ -213,13 +227,15 @@ private fun NiaNavRail(
imageVector = icon.imageVector,
contentDescription = null,
)

is DrawableResourceIcon -> Icon(
painter = painterResource(id = icon.id),
contentDescription = null,
)
}
},
label = { Text(stringResource(destination.iconTextId)) },

)
}
}
Expand All @@ -228,6 +244,7 @@ private fun NiaNavRail(
@Composable
private fun NiaBottomBar(
destinations: List<TopLevelDestination>,
destinationsWithUnreadResources: Set<TopLevelDestination>,
onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?,
modifier: Modifier = Modifier,
Expand All @@ -236,6 +253,7 @@ private fun NiaBottomBar(
modifier = modifier,
) {
destinations.forEach { destination ->
val hasUnread = destinationsWithUnreadResources.contains(destination)
val selected = currentDestination.isTopLevelDestinationInHierarchy(destination)
NiaNavigationBarItem(
selected = selected,
Expand All @@ -259,6 +277,25 @@ private fun NiaBottomBar(
}
},
label = { Text(stringResource(destination.iconTextId)) },
modifier = if (hasUnread) {
val tertiaryColor = MaterialTheme.colorScheme.tertiary
Modifier.drawWithContent {
drawContent()
drawCircle(
tertiaryColor,
radius = 5.dp.toPx(),
// This is based on the dimensions of the NavigationBar's "indicator pill";
// however, its parameters are private, so we must depend on them implicitly
// (NavigationBarTokens.ActiveIndicatorWidth = 64.dp)
center = center + Offset(
64.dp.toPx() * .45f,
32.dp.toPx() * -.45f - 6.dp.toPx(),
),
)
}
} else {
Modifier
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class NewsResourceCardTest {
NewsResourceCardExpanded(
userNewsResource = newsWithKnownResourceType,
isBookmarked = false,
isViewed = false,
onToggleBookmark = {},
onClick = {},
onTopicClick = {},
Expand Down Expand Up @@ -67,6 +68,7 @@ class NewsResourceCardTest {
NewsResourceCardExpanded(
userNewsResource = newsWithUnknownResourceType,
isBookmarked = false,
isViewed = false,
onToggleBookmark = {},
onClick = {},
onTopicClick = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
fun LazyGridScope.newsFeed(
feedState: NewsFeedUiState,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onTopicClick: (String) -> Unit,
) {
when (feedState) {
Expand All @@ -70,7 +71,9 @@ fun LazyGridScope.newsFeed(
newsResourceTitle = userNewsResource.title,
)
launchCustomChromeTab(context, resourceUrl, backgroundColor)
onNewsResourcesViewedChanged(userNewsResource.id, true)
},
isViewed = userNewsResource.isViewed,
onToggleBookmark = {
onNewsResourcesCheckedChanged(
userNewsResource.id,
Expand Down Expand Up @@ -122,6 +125,7 @@ private fun NewsFeedLoadingPreview() {
newsFeed(
feedState = NewsFeedUiState.Loading,
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onTopicClick = {},
)
}
Expand All @@ -140,6 +144,7 @@ private fun NewsFeedContentPreview(
newsFeed(
feedState = NewsFeedUiState.Success(userNewsResources),
onNewsResourcesCheckedChanged = { _, _ -> },
onNewsResourcesViewedChanged = { _, _ -> },
onTopicClick = {},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.samples.apps.nowinandroid.core.ui

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand All @@ -25,6 +26,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
Expand All @@ -40,7 +42,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
Expand Down Expand Up @@ -77,6 +81,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.R as DesignsystemR
fun NewsResourceCardExpanded(
userNewsResource: UserNewsResource,
isBookmarked: Boolean,
isViewed: Boolean,
onToggleBookmark: () -> Unit,
onClick: () -> Unit,
onTopicClick: (String) -> Unit,
Expand Down Expand Up @@ -113,7 +118,16 @@ fun NewsResourceCardExpanded(
BookmarkButton(isBookmarked, onToggleBookmark)
}
Spacer(modifier = Modifier.height(12.dp))
NewsResourceMetaData(userNewsResource.publishDate, userNewsResource.type)
Row(verticalAlignment = Alignment.CenterVertically) {
if (!isViewed) {
Dot(
color = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.size(8.dp),
)
Spacer(modifier = Modifier.size(6.dp))
}
NewsResourceMetaData(userNewsResource.publishDate, userNewsResource.type)
}
Spacer(modifier = Modifier.height(12.dp))
NewsResourceShortDescription(userNewsResource.content)
Spacer(modifier = Modifier.height(12.dp))
Expand Down Expand Up @@ -181,6 +195,22 @@ fun BookmarkButton(
)
}

@Composable
fun Dot(
color: Color,
modifier: Modifier = Modifier,
) {
Canvas(
modifier = modifier,
onDraw = {
drawCircle(
color,
radius = size.minDimension / 2,
)
},
)
}

@Composable
fun dateFormatted(publishDate: Instant): String {
var zoneId by remember { mutableStateOf(ZoneId.systemDefault()) }
Expand Down Expand Up @@ -301,6 +331,7 @@ private fun ExpandedNewsResourcePreview(
NewsResourceCardExpanded(
userNewsResource = userNewsResources[0],
isBookmarked = true,
isViewed = false,
onToggleBookmark = {},
onClick = {},
onTopicClick = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
fun LazyListScope.userNewsResourceCardItems(
items: List<UserNewsResource>,
onToggleBookmark: (item: UserNewsResource) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onItemClick: ((item: UserNewsResource) -> Unit)? = null,
onTopicClick: (String) -> Unit,
itemModifier: Modifier = Modifier,
Expand All @@ -52,14 +53,18 @@ fun LazyListScope.userNewsResourceCardItems(
NewsResourceCardExpanded(
userNewsResource = userNewsResource,
isBookmarked = userNewsResource.isSaved,
isViewed = userNewsResource.isViewed,
onToggleBookmark = { onToggleBookmark(userNewsResource) },
onClick = {
analyticsHelper.logNewsResourceOpened(
newsResourceId = userNewsResource.id,
newsResourceTitle = userNewsResource.title,
)
when (onItemClick) {
null -> launchCustomChromeTab(context, resourceUrl, backgroundColor)
null -> {
launchCustomChromeTab(context, resourceUrl, backgroundColor)
onNewsResourcesViewedChanged(userNewsResource.id, true)
}
else -> onItemClick(userNewsResource)
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ internal fun BookmarksRoute(
BookmarksScreen(
feedState = feedState,
removeFromBookmarks = viewModel::removeFromSavedResources,
onNewsResourcesViewedChanged = viewModel::updateNewsResourceViewed,
onTopicClick = onTopicClick,
modifier = modifier,
)
Expand All @@ -88,13 +89,14 @@ internal fun BookmarksRoute(
internal fun BookmarksScreen(
feedState: NewsFeedUiState,
removeFromBookmarks: (String) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
when (feedState) {
Loading -> LoadingState(modifier)
is Success -> if (feedState.feed.isNotEmpty()) {
BookmarksGrid(feedState, removeFromBookmarks, onTopicClick, modifier)
BookmarksGrid(feedState, removeFromBookmarks, onNewsResourcesViewedChanged, onTopicClick, modifier)
} else {
EmptyState(modifier)
}
Expand All @@ -117,6 +119,7 @@ private fun LoadingState(modifier: Modifier = Modifier) {
private fun BookmarksGrid(
feedState: NewsFeedUiState,
removeFromBookmarks: (String) -> Unit,
onNewsResourcesViewedChanged: (String, Boolean) -> Unit,
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Expand All @@ -135,6 +138,7 @@ private fun BookmarksGrid(
newsFeed(
feedState = feedState,
onNewsResourcesCheckedChanged = { id, _ -> removeFromBookmarks(id) },
onNewsResourcesViewedChanged = onNewsResourcesViewedChanged,
onTopicClick = onTopicClick,
)
item(span = { GridItemSpan(maxLineSpan) }) {
Expand Down Expand Up @@ -200,6 +204,7 @@ private fun BookmarksGridPreview(
BookmarksGrid(
feedState = Success(userNewsResources),
removeFromBookmarks = {},
onNewsResourcesViewedChanged = { _, _ -> },
onTopicClick = {},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,10 @@ class BookmarksViewModel @Inject constructor(
userDataRepository.updateNewsResourceBookmark(newsResourceId, false)
}
}

fun updateNewsResourceViewed(newsResourceId: String, isViewed: Boolean) {
viewModelScope.launch {
userDataRepository.updateNewsResourceViewed(newsResourceId, isViewed)
}
}
}
Loading

0 comments on commit 0a5d076

Please sign in to comment.