diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-08-22 01:13:30 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-08-22 01:13:30 +0000 |
| commit | 55cdfc8fab6ddd1052593f8fd53dbb5bf730cc63 (patch) | |
| tree | 2c44a9e63e89dd925c98b0606c5c1e6e29f48d10 /app/src/main/java/org/terst/feedreader/ui | |
| parent | a5f728648dda46e84959a4568a3f04d5cc7bb11f (diff) | |
Keep a just-read article visible (and Previous-reachable) until the next fetch
The Unread/Older views are live Room Flow queries filtered on
isRead = 0, so marking an article read (which only happens on
Next/Previous, not on open) makes it vanish from the list the instant
Room re-emits -- mid-scroll, or from underneath the reader's own
Previous button, which re-queries this same flow to find its
neighbors. Concretely: Next from C to D marks C read and prunes it
from the live list; Previous from D then can't find C anymore, and
jumps past it to whatever was unread before C. A misclick or the
in-flight prune could cost you the article you meant to keep reading.
Adds ArticleEntity.readAt (set whenever isRead flips true, cleared on
explicit "mark unread"); observeUnread/observeOlderUnread now match
"isRead = 0 OR readAt >= sinceFetch", where sinceFetch is
Settings.lastFetchAt -- the same durable signal the new-articles
snackbar already uses. A read article now lingers (shown dimmed via
the existing showReadState styling) until the next real fetch
actually completes, not the next DB write, and Previous/Next read from
the same still-inclusive query so stepping back to what you just read
finds it right where it was.
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/ui')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt | 21 | ||||
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt | 15 |
2 files changed, 26 insertions, 10 deletions
diff --git a/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt index 9d88374..a3edebe 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ArticleListScreen.kt @@ -43,8 +43,10 @@ import androidx.compose.ui.unit.dp import androidx.work.WorkInfo import androidx.work.WorkManager import coil.compose.AsyncImage +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import org.terst.feedreader.data.AppDatabase import org.terst.feedreader.data.ArticleListItem @@ -58,7 +60,7 @@ import java.util.Date private const val TAG = "ArticleListScreen" -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalCoroutinesApi::class) @Composable fun ArticleListScreen( feedId: String, @@ -70,11 +72,16 @@ fun ArticleListScreen( val db = remember { AppDatabase.get(context) } val scope = rememberCoroutineScope() val cutoff = remember(feedId) { System.currentTimeMillis() - RECENT_WINDOW_MS } + val settings = remember { Settings(context) } + // Unread/Older re-derive their query every time lastFetchAt changes (flatMapLatest), + // not just once at screen entry -- a fetch completing while this screen is open should + // immediately drop whatever was read-but-lingering, same as if you'd navigated back in. + // See observeUnread's comment for why sinceFetch is here at all. val articleFlow = remember(feedId) { when (feedId) { - UNREAD_FEED_ID -> db.articleDao().observeUnread(cutoff) - OLDER_FEED_ID -> db.articleDao().observeOlderUnread(cutoff) + UNREAD_FEED_ID -> settings.lastFetchAt.flatMapLatest { since -> db.articleDao().observeUnread(cutoff, since) } + OLDER_FEED_ID -> settings.lastFetchAt.flatMapLatest { since -> db.articleDao().observeOlderUnread(cutoff, since) } SAVED_FEED_ID -> db.articleDao().observeSaved() else -> db.articleDao().observeByFeed(feedId) } @@ -99,7 +106,6 @@ fun ArticleListScreen( val showFetchStatus = feedId == UNREAD_FEED_ID || feedId == OLDER_FEED_ID val snackbarHostState = remember { SnackbarHostState() } - val settings = remember { Settings(context) } val fetchWorkInfos by remember { WorkManager.getInstance(context).getWorkInfosByTagFlow(FeedFetchWorker.FETCH_WORK_TAG) // A pure status readout, not load-bearing for the article list itself -- an @@ -223,6 +229,11 @@ fun ArticleListScreen( } } +// Shared with ReaderWebView's wrapReaderHtml (the reader's own date header) so both +// places format a publishedAt timestamp identically. +fun absoluteDateText(timestamp: Long): String = + DateFormat.getDateInstance(DateFormat.MEDIUM).format(Date(timestamp)) + @OptIn(ExperimentalFoundationApi::class) @Composable private fun ArticleRow( @@ -233,7 +244,7 @@ private fun ArticleRow( onClick: () -> Unit, onMarkReadFrom: () -> Unit ) { - val dateText = article.publishedAt?.let { DateFormat.getDateInstance(DateFormat.MEDIUM).format(Date(it)) } + val dateText = article.publishedAt?.let { absoluteDateText(it) } val metaText = listOfNotNull(if (showFeedName) article.feedTitle else null, dateText) .joinToString(" · ") .ifEmpty { null } diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt index 4317073..4c74e5a 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt @@ -57,6 +57,7 @@ import kotlinx.coroutines.launch import org.terst.feedreader.data.AppDatabase import org.terst.feedreader.data.ArticleEntity import org.terst.feedreader.data.FeedEntity +import org.terst.feedreader.data.Settings import org.terst.feedreader.data.ensureStandaloneArticle import org.terst.feedreader.data.saveArticleToStash import org.terst.feedreader.data.setReaderModeEnabled @@ -96,6 +97,7 @@ fun ReaderScreen( ) { val context = LocalContext.current val db = remember { AppDatabase.get(context) } + val settings = remember { Settings(context) } val scope = rememberCoroutineScope() var previousId by remember(articleId) { mutableStateOf<String?>(null) } @@ -104,11 +106,14 @@ fun ReaderScreen( // Looked up fresh every time articleId changes (i.e. every navigation), not cached -- // always reflects the CURRENT live Unread/Older/Saved/feed ordering, so there's no // stale-snapshot state to fall out of sync with anything, on rotation or otherwise. + // sinceFetch (Settings.lastFetchAt) is what keeps a just-read article from vanishing + // out of this same list before Previous/Next can find it -- see observeUnread's comment. LaunchedEffect(articleId) { val cutoff = System.currentTimeMillis() - RECENT_WINDOW_MS + val sinceFetch = settings.lastFetchAt.first() val flow = when (feedId) { - UNREAD_FEED_ID -> db.articleDao().observeUnread(cutoff) - OLDER_FEED_ID -> db.articleDao().observeOlderUnread(cutoff) + UNREAD_FEED_ID -> db.articleDao().observeUnread(cutoff, sinceFetch) + OLDER_FEED_ID -> db.articleDao().observeOlderUnread(cutoff, sinceFetch) SAVED_FEED_ID -> db.articleDao().observeSaved() else -> db.articleDao().observeByFeed(feedId) } @@ -124,14 +129,14 @@ fun ReaderScreen( fun goToPreviousArticle() { val target = previousId ?: return scope.launch { - db.articleDao().setRead(articleId, true) + db.articleDao().setRead(articleId, true, System.currentTimeMillis()) onNavigateToArticle(target) } } fun goToNextArticle() { val target = nextId ?: return scope.launch { - db.articleDao().setRead(articleId, true) + db.articleDao().setRead(articleId, true, System.currentTimeMillis()) onNavigateToArticle(target) } } @@ -345,7 +350,7 @@ private fun ReaderContent( val a = currentArticle ?: return@DropdownMenuItem article = a.copy(isRead = false) scope.launch { - db.articleDao().setRead(a.id, false) + db.articleDao().setRead(a.id, false, null) snackbarHostState.showSnackbar("Marked unread") } } |
