From 86596916a95c671749b1746e4f22c8b8da46ee05 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Thu, 18 Apr 2024 06:52:11 +0530 Subject: [PATCH] Show unread posts count in groups (#463) * Update source queries to include number of unread posts count for groups * Show unread posts badge for feed group items --- .../rss/reader/core/model/local/FeedGroup.kt | 1 + .../feeds/ui/BottomSheetCollapsedContent.kt | 1 + .../feeds/ui/BottomSheetExpandedContent.kt | 22 +++-- .../rss/reader/feeds/ui/FeedBottomBarItem.kt | 3 +- .../reader/feeds/ui/FeedGroupBottomBarItem.kt | 94 +++++++++++++------ .../rss/reader/feeds/ui/FeedGroupItem.kt | 18 ++++ .../groupselection/ui/GroupSelectionSheet.kt | 1 + .../rss/reader/repository/RssRepository.kt | 2 + .../sasikanth/rss/reader/utils/Constants.kt | 2 + .../sasikanth/rss/reader/database/Source.sq | 8 +- 10 files changed, 108 insertions(+), 44 deletions(-) diff --git a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt index 218899b7b..25a283329 100644 --- a/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt +++ b/core/model/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/model/local/FeedGroup.kt @@ -23,6 +23,7 @@ data class FeedGroup( val name: String, val feedIds: List, val feedIcons: List, + val numberOfUnreadPosts: Long = 0, val createdAt: Instant, val updatedAt: Instant, override val pinnedAt: Instant?, diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt index 153697b2e..f321520a3 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetCollapsedContent.kt @@ -83,6 +83,7 @@ internal fun BottomSheetCollapsedContent( is FeedGroup -> { FeedGroupBottomBarItem( feedGroup = source, + canShowUnreadPostsCount = canShowUnreadPostsCount, selected = activeSource?.id == source.id, onClick = { onSourceClick(source) } ) diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt index 2b05b3b15..f47061b84 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/BottomSheetExpandedContent.kt @@ -498,6 +498,12 @@ private fun LazyGridScope.allSources( when (source) { is FeedGroup -> { FeedGroupItem( + feedGroup = source, + canShowUnreadPostsCount = canShowUnreadPostsCount, + isInMultiSelectMode = isInMultiSelectMode, + selected = selectedSources.contains(source), + onFeedGroupSelected = onToggleSourceSelection, + onFeedGroupClick = onSourceClick, modifier = Modifier.padding( start = startPadding, @@ -505,11 +511,6 @@ private fun LazyGridScope.allSources( end = endPadding, bottom = bottomPadding ), - feedGroup = source, - isInMultiSelectMode = isInMultiSelectMode, - selected = selectedSources.contains(source), - onFeedGroupSelected = onToggleSourceSelection, - onFeedGroupClick = onSourceClick ) } is Feed -> { @@ -582,6 +583,12 @@ private fun LazyGridScope.pinnedSources( when (source) { is FeedGroup -> { FeedGroupItem( + feedGroup = source, + canShowUnreadPostsCount = canShowUnreadPostsCount, + isInMultiSelectMode = isInMultiSelectMode, + selected = selectedSources.contains(source), + onFeedGroupSelected = onToggleSourceSelection, + onFeedGroupClick = onSourceClick, modifier = Modifier.padding( start = startPadding, @@ -589,11 +596,6 @@ private fun LazyGridScope.pinnedSources( end = endPadding, bottom = bottomPadding ), - feedGroup = source, - isInMultiSelectMode = isInMultiSelectMode, - selected = selectedSources.contains(source), - onFeedGroupSelected = onToggleSourceSelection, - onFeedGroupClick = onSourceClick ) } is Feed -> { diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedBottomBarItem.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedBottomBarItem.kt index a363374f3..74098d7af 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedBottomBarItem.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedBottomBarItem.kt @@ -38,8 +38,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import dev.sasikanth.rss.reader.components.image.AsyncImage import dev.sasikanth.rss.reader.ui.AppTheme - -private const val BADGE_COUNT_TRIM_LIMIT = 99 +import dev.sasikanth.rss.reader.utils.Constants.BADGE_COUNT_TRIM_LIMIT @Composable internal fun FeedBottomBarItem( diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedGroupBottomBarItem.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedGroupBottomBarItem.kt index ffc994ad5..17f6d2253 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedGroupBottomBarItem.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedGroupBottomBarItem.kt @@ -21,55 +21,89 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Badge +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import dev.sasikanth.rss.reader.core.model.local.FeedGroup import dev.sasikanth.rss.reader.ui.AppTheme +import dev.sasikanth.rss.reader.utils.Constants.BADGE_COUNT_TRIM_LIMIT @Composable internal fun FeedGroupBottomBarItem( feedGroup: FeedGroup, + canShowUnreadPostsCount: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, selected: Boolean = false, ) { - Box(modifier = modifier, contentAlignment = Alignment.Center) { - SelectionIndicator(selected = selected, animationProgress = 1f) - Box( - modifier = - Modifier.requiredSize(56.dp) - .clip(RoundedCornerShape(16.dp)) - .clickable { onClick() } - .background(AppTheme.colorScheme.tintedSurface) - .padding(8.dp), - contentAlignment = Alignment.Center - ) { - val iconSize = - if (feedGroup.feedIcons.size > 2) { - 18.dp - } else { - 20.dp - } + Box(modifier = modifier) { + Box(contentAlignment = Alignment.Center) { + SelectionIndicator(selected = selected, animationProgress = 1f) + Box( + modifier = + Modifier.requiredSize(56.dp) + .clip(RoundedCornerShape(16.dp)) + .clickable { onClick() } + .background(AppTheme.colorScheme.tintedSurface) + .padding(8.dp), + contentAlignment = Alignment.Center + ) { + val iconSize = + if (feedGroup.feedIcons.size > 2) { + 18.dp + } else { + 20.dp + } - val iconSpacing = - if (feedGroup.feedIcons.size > 2) { - 4.dp - } else { - 0.dp - } + val iconSpacing = + if (feedGroup.feedIcons.size > 2) { + 4.dp + } else { + 0.dp + } - FeedGroupIconGrid( - icons = feedGroup.feedIcons, - iconSize = iconSize, - iconShape = CircleShape, - verticalArrangement = Arrangement.spacedBy(iconSpacing), - horizontalArrangement = Arrangement.spacedBy(iconSpacing), - ) + FeedGroupIconGrid( + icons = feedGroup.feedIcons, + iconSize = iconSize, + iconShape = CircleShape, + verticalArrangement = Arrangement.spacedBy(iconSpacing), + horizontalArrangement = Arrangement.spacedBy(iconSpacing), + ) + } + } + + val badgeCount = feedGroup.numberOfUnreadPosts + if (badgeCount > 0 && canShowUnreadPostsCount) { + Badge( + containerColor = AppTheme.colorScheme.tintedForeground, + contentColor = AppTheme.colorScheme.tintedBackground, + modifier = Modifier.sizeIn(minWidth = 24.dp, minHeight = 16.dp).align(Alignment.TopEnd), + ) { + val badgeText = + if (badgeCount > BADGE_COUNT_TRIM_LIMIT) { + "+$BADGE_COUNT_TRIM_LIMIT" + } else { + badgeCount.toString() + } + + Text( + text = badgeText, + style = MaterialTheme.typography.labelSmall, + modifier = + Modifier.align(Alignment.CenterVertically).graphicsLayer { + translationY = -2.toDp().toPx() + } + ) + } } } } diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedGroupItem.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedGroupItem.kt index f815fe2a9..de393d325 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedGroupItem.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/feeds/ui/FeedGroupItem.kt @@ -28,7 +28,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Badge import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -51,6 +53,7 @@ import dev.sasikanth.rss.reader.ui.AppTheme @Composable internal fun FeedGroupItem( feedGroup: FeedGroup, + canShowUnreadPostsCount: Boolean, isInMultiSelectMode: Boolean, selected: Boolean, onFeedGroupSelected: (FeedGroup) -> Unit, @@ -140,6 +143,21 @@ internal fun FeedGroupItem( Spacer(Modifier.requiredWidth(4.dp)) + val numberOfUnreadPosts = feedGroup.numberOfUnreadPosts + if (canShowUnreadPostsCount && numberOfUnreadPosts > 0 && !isInMultiSelectMode) { + Badge( + containerColor = AppTheme.colorScheme.tintedForeground, + contentColor = AppTheme.colorScheme.tintedBackground, + modifier = Modifier.sizeIn(minWidth = 24.dp, minHeight = 16.dp) + ) { + Text( + text = feedGroup.numberOfUnreadPosts.toString(), + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + } + if (isInMultiSelectMode) { val icon = if (selected) { diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt index 706c23e88..47bd94d8a 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/groupselection/ui/GroupSelectionSheet.kt @@ -132,6 +132,7 @@ fun GroupSelectionSheet(presenter: GroupSelectionPresenter, modifier: Modifier = if (group != null) { FeedGroupItem( feedGroup = group, + canShowUnreadPostsCount = false, isInMultiSelectMode = true, selected = state.selectedGroups.contains(group.id), onFeedGroupSelected = { diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt index ead9b52d1..b2736fdf1 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/repository/RssRepository.kt @@ -582,6 +582,7 @@ class RssRepository( createdAt = createdAt!!, updatedAt = updatedAt!!, pinnedAt = pinnedAt, + numberOfUnreadPosts = numberOfUnreadPosts, ) } else { Feed( @@ -641,6 +642,7 @@ class RssRepository( createdAt = createdAt, updatedAt = updatedAt!!, pinnedAt = pinnedAt, + numberOfUnreadPosts = numberOfUnreadPosts, ) } else { Feed( diff --git a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/Constants.kt b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/Constants.kt index 9f439a0a8..067c2f7f3 100644 --- a/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/Constants.kt +++ b/shared/src/commonMain/kotlin/dev/sasikanth/rss/reader/utils/Constants.kt @@ -38,4 +38,6 @@ internal object Constants { const val OPEN_SOURCE_LINK = "https://github.com/sponsors/msasikanth" const val MINIMUM_REQUIRED_SEARCH_CHARACTERS = 3 + + const val BADGE_COUNT_TRIM_LIMIT = 99 } diff --git a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Source.sq b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Source.sq index d12b80c0c..dbcc448d1 100644 --- a/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Source.sq +++ b/shared/src/commonMain/sqldelight/dev/sasikanth/rss/reader/database/Source.sq @@ -65,7 +65,7 @@ FROM ( NULL AS link, NULL AS homepageLink, NULL AS lastCleanUpAt, - 0 AS numberOfUnreadPosts, + COUNT(CASE WHEN p.read == 0 THEN 1 ELSE NULL END) AS numberOfUnreadPosts, fg.feedIds, COALESCE((SELECT GROUP_CONCAT(feed.icon) FROM feed @@ -75,6 +75,8 @@ FROM ( fg.pinnedAt, fg.updatedAt FROM feedGroup fg + LEFT JOIN post p ON INSTR(fg.feedIds, p.sourceId) AND p.date > :postsAfter + GROUP BY fg.id ) WHERE pinnedAt IS NOT NULL ORDER BY pinnedAt DESC @@ -127,7 +129,7 @@ FROM ( NULL AS link, NULL AS homepageLink, NULL AS lastCleanUpAt, - 0 AS numberOfUnreadPosts, + COUNT(CASE WHEN p.read == 0 THEN 1 ELSE NULL END) AS numberOfUnreadPosts, fg.feedIds, COALESCE((SELECT GROUP_CONCAT(feed.icon) FROM feed @@ -137,6 +139,8 @@ FROM ( fg.pinnedAt, fg.updatedAt FROM feedGroup fg + LEFT JOIN post p ON INSTR(fg.feedIds, p.sourceId) AND p.date > :postsAfter + GROUP BY fg.id ) ORDER BY type DESC, CASE WHEN :orderBy = 'latest' THEN createdAt END DESC,