Skip to content

Commit

Permalink
Display notifications per-article instead of feed
Browse files Browse the repository at this point in the history
Groups notifications by article by feed with
individual notifications for each.
  • Loading branch information
jocmp committed Oct 17, 2024
1 parent fd77657 commit 6eb6161
Show file tree
Hide file tree
Showing 18 changed files with 265 additions and 182 deletions.
7 changes: 4 additions & 3 deletions app/src/main/java/com/capyreader/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import android.os.StrictMode.setThreadPolicy
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.capyreader.app.common.AppPreferences
import com.capyreader.app.refresher.FeedNotifications
import com.capyreader.app.refresher.ArticleNotifications
import com.capyreader.app.ui.App
import com.capyreader.app.ui.Route
import org.koin.android.ext.android.get
Expand All @@ -23,7 +24,7 @@ class MainActivity : ComponentActivity() {
enableStrictModeOnDebug()
enableEdgeToEdge()
super.onCreate(savedInstanceState)
FeedNotifications.handleResult(intent, appPreferences = appPreferences)
ArticleNotifications.handleResult(intent, appPreferences = appPreferences)

val theme = appPreferences.theme

Expand All @@ -39,7 +40,7 @@ class MainActivity : ComponentActivity() {

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
FeedNotifications.handleResult(intent, appPreferences = appPreferences)
ArticleNotifications.handleResult(intent, appPreferences = appPreferences)
}

private fun startDestination(): String {
Expand Down
129 changes: 129 additions & 0 deletions app/src/main/java/com/capyreader/app/refresher/ArticleNotifications.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.capyreader.app.refresher

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.Style
import com.capyreader.app.MainActivity
import com.capyreader.app.Notifications.FEED_UPDATE
import com.capyreader.app.R
import com.capyreader.app.common.AppPreferences
import com.capyreader.app.common.notificationManager
import com.capyreader.app.refresher.ArticleNotifications.Companion.ARTICLE_ID_KEY
import com.capyreader.app.refresher.ArticleNotifications.Companion.FEED_ID_KEY
import com.jocmp.capy.Account
import com.jocmp.capy.ArticleFilter
import com.jocmp.capy.ArticleStatus
import com.jocmp.capy.notifications.ArticleNotification
import java.lang.reflect.Field
import java.time.ZonedDateTime

class ArticleNotifications(
private val account: Account,
private val applicationContext: Context,
) {
private val notificationManager = applicationContext.notificationManager

suspend fun notify(since: ZonedDateTime) {
createChannel()

account.findNotifications(since = since)
.grouped()
.forEach {
notify(it)
}
}

private fun notify(group: FeedNotification) {
val builder = NotificationCompat.Builder(applicationContext, FEED_UPDATE.channelID)
.setContentTitle(group.title)
.setSmallIcon(R.drawable.newsmode)
.setGroup(group.id)
.setGroupSummary(true)
.setStyle(group.inboxStyle())

group.notifications.forEach { notifyArticle(it) }

notificationManager.notify(group.notificationID, builder.build())
}

private fun notifyArticle(notification: ArticleNotification) {
val builder = NotificationCompat.Builder(applicationContext, FEED_UPDATE.channelID)
.setContentTitle(notification.title)
.setSmallIcon(R.drawable.newsmode)
.setGroup(notification.feedID)
.setSubText(notification.feedTitle)
.setAutoCancel(true)
.setContentIntent(notification.intent(applicationContext))

notificationManager.notify(notification.notificationID, builder.build())
}

private fun createChannel() {
val name = applicationContext.getString(R.string.notifications_channel_title_feed_update)
val channel = NotificationChannel(
FEED_UPDATE.channelID,
name,
NotificationManager.IMPORTANCE_DEFAULT
)

notificationManager.createNotificationChannel(channel)
}

companion object {
const val ARTICLE_ID_KEY = "article_id"
const val FEED_ID_KEY = "feed_id"

fun handleResult(intent: Intent, appPreferences: AppPreferences) {
val articleID = intent.getStringExtra(ARTICLE_ID_KEY) ?: return
val feedID = intent.getStringExtra(FEED_ID_KEY) ?: return
intent.replaceExtras(Bundle())

appPreferences.filter.set(
ArticleFilter.Feeds(
feedID,
feedStatus = ArticleStatus.ALL
)
)
appPreferences.articleID.set(articleID)
}
}
}

private fun FeedNotification.inboxStyle(): Style {
val style = NotificationCompat.InboxStyle()

notifications.take(3).forEach {
style.addLine(it.title)
}

style.setSummaryText(title)

return style
}


private fun ArticleNotification.intent(context: Context): PendingIntent {
val notifyIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(ARTICLE_ID_KEY, id)
putExtra(FEED_ID_KEY, feedID)
}

return PendingIntent.getActivity(
context,
notificationID,
notifyIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}

private fun List<ArticleNotification>.grouped() =
groupBy { it.feedID }.map { (feedID, notifications) ->
FeedNotification.from(feedID, notifications)
}
30 changes: 30 additions & 0 deletions app/src/main/java/com/capyreader/app/refresher/FeedNotification.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.capyreader.app.refresher

import com.jocmp.capy.notifications.ArticleNotification

internal data class FeedNotification(
val id: String,
val title: String,
val faviconURL: String? = null,
val notifications: List<ArticleNotification>
) {
val articleCount: Int
get() = notifications.size

val notificationID
get() = id.hashCode()

companion object {
fun from(feedID: String, notifications: List<ArticleNotification>): FeedNotification {
val title = notifications.firstOrNull()?.feedTitle.orEmpty()
val faviconURL = notifications.firstOrNull()?.feedFaviconURL

return FeedNotification(
id = feedID,
title = title,
faviconURL = faviconURL,
notifications = notifications
)
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class FeedRefresher(
private val account: Account,
applicationContext: Context,
) {
private val notifications = FeedNotifications(account = account, applicationContext)
private val notifications = ArticleNotifications(account = account, applicationContext)

suspend fun refresh() {
val since = TimeHelpers.nowUTC()
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/java/com/capyreader/app/ui/articles/ArticleHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.capyreader.app.ui.articles

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.capyreader.app.common.AppPreferences
import org.koin.compose.koinInject

@Composable
fun ArticleHandler(
appPreferences: AppPreferences = koinInject(),
onRequestArticle: (articleID: String) -> Unit,
) {
LaunchedEffect(Unit) {
val articleID = appPreferences.articleID.get()

if (articleID.isNotBlank()) {
appPreferences.articleID.delete()
onRequestArticle(articleID)
}
}
}
39 changes: 16 additions & 23 deletions app/src/main/java/com/capyreader/app/ui/articles/ArticleLayout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TopAppBarDefaults.pinnedScrollBehavior
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.rememberDrawerState
Expand Down Expand Up @@ -90,7 +89,7 @@ fun ArticleLayout(
onSelectFeed: suspend (feedID: String) -> Unit,
onSelectArticleFilter: () -> Unit,
onSelectStatus: (status: ArticleStatus) -> Unit,
onSelectArticle: suspend (articleID: String) -> Unit,
onSelectArticle: (articleID: String) -> Unit,
onNavigateToSettings: () -> Unit,
onRequestClearArticle: () -> Unit,
onToggleArticleRead: () -> Unit,
Expand Down Expand Up @@ -224,6 +223,20 @@ fun ArticleLayout(
}
}

val selectArticle = { articleID: String ->
onSelectArticle(articleID)
if (search.isActive) {
focusManager.clearFocus()
}
coroutineScope.launch {
navigateToDetail()
}
}

ArticleHandler {
selectArticle(it)
}

ArticleScaffold(
drawerState = drawerState,
scaffoldNavigator = scaffoldNavigator,
Expand Down Expand Up @@ -337,13 +350,7 @@ fun ArticleLayout(
selectedArticleKey = article?.id,
listState = listState,
onMarkAllRead = onMarkAllRead,
onSelect = { articleID ->
onSelectArticle(articleID)
if (search.isActive) {
focusManager.clearFocus()
}
navigateToDetail()
},
onSelect = { selectArticle(it) },
)
}
}
Expand Down Expand Up @@ -431,8 +438,6 @@ fun ArticleLayout(
}
}

ResetPageOnClear(article, scaffoldNavigator)

BackHandler(mediaUrl != null) {
mediaUrl = null
}
Expand All @@ -448,18 +453,6 @@ fun ArticleLayout(
}
}

@Composable
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
fun ResetPageOnClear(article: Article?, navigator: ThreePaneScaffoldNavigator<Any>) {
LaunchedEffect(article, navigator) {
val isReader = navigator.currentDestination?.pane == ListDetailPaneScaffoldRole.Detail

if (isReader && article == null) {
navigator.navigateTo(ListDetailPaneScaffoldRole.List)
}
}
}

fun isFeedActive(
mediaURL: String?,
article: Article?,
Expand Down
Loading

0 comments on commit 6eb6161

Please sign in to comment.