summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/data/Daos.kt
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/data/Daos.kt')
-rw-r--r--app/src/main/java/org/terst/feedreader/data/Daos.kt39
1 files changed, 26 insertions, 13 deletions
diff --git a/app/src/main/java/org/terst/feedreader/data/Daos.kt b/app/src/main/java/org/terst/feedreader/data/Daos.kt
index ecaac43..20632f8 100644
--- a/app/src/main/java/org/terst/feedreader/data/Daos.kt
+++ b/app/src/main/java/org/terst/feedreader/data/Daos.kt
@@ -107,21 +107,30 @@ interface ArticleDao {
// MainActivity) so a backlog of hundreds of older unread items doesn't bury
// what's actually new. Undated articles (publishedAt failed to parse) are kept
// here rather than silently dropped from both views.
+ //
+ // "isRead = 0 OR readAt >= sinceFetch" (not just "isRead = 0"): a just-read article
+ // stays in this view until the NEXT fetch completes, not the instant it's marked
+ // read. Without this, Room's live Flow re-emits the moment isRead flips, so (a) a
+ // read row vanishes out from under you mid-scroll, and (b) the reader's Previous
+ // button -- which re-queries this same flow to find its neighbors -- can no longer
+ // find the article you just came from, since Next already marked it read a moment
+ // earlier. sinceFetch is Settings.lastFetchAt, the same durable "when did a fetch
+ // last finish" signal the new-articles snackbar already uses.
@Query(
"SELECT $ARTICLE_LIST_COLUMNS FROM $ARTICLE_LIST_FROM " +
- "WHERE articles.isRead = 0 AND (articles.publishedAt IS NULL OR articles.publishedAt >= :cutoff) " +
+ "WHERE (articles.isRead = 0 OR articles.readAt >= :sinceFetch) AND (articles.publishedAt IS NULL OR articles.publishedAt >= :cutoff) " +
"ORDER BY articles.publishedAt DESC"
)
- fun observeUnread(cutoff: Long): Flow<List<ArticleListItem>>
+ fun observeUnread(cutoff: Long, sinceFetch: Long): Flow<List<ArticleListItem>>
// The rest of the unread backlog, older than the cutoff -- its own view rather
- // than mixed into the main one.
+ // than mixed into the main one. See observeUnread for why sinceFetch is here.
@Query(
"SELECT $ARTICLE_LIST_COLUMNS FROM $ARTICLE_LIST_FROM " +
- "WHERE articles.isRead = 0 AND articles.publishedAt IS NOT NULL AND articles.publishedAt < :cutoff " +
+ "WHERE (articles.isRead = 0 OR articles.readAt >= :sinceFetch) AND articles.publishedAt IS NOT NULL AND articles.publishedAt < :cutoff " +
"ORDER BY articles.publishedAt DESC"
)
- fun observeOlderUnread(cutoff: Long): Flow<List<ArticleListItem>>
+ fun observeOlderUnread(cutoff: Long, sinceFetch: Long): Flow<List<ArticleListItem>>
@Query("SELECT COUNT(*) FROM articles WHERE isRead = 0 AND (publishedAt IS NULL OR publishedAt >= :cutoff)")
fun observeUnreadCount(cutoff: Long): Flow<Int>
@@ -157,16 +166,20 @@ interface ArticleDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(article: ArticleEntity)
- @Query("UPDATE articles SET isRead = :read WHERE id = :id")
- suspend fun setRead(id: String, read: Boolean)
+ // readAt is caller-supplied (not CURRENT_TIMESTAMP in SQL) so it's directly comparable
+ // to Settings.lastFetchAt, which is also a Kotlin System.currentTimeMillis() epoch
+ // value, not a SQLite datetime string. Pass null when read=false (explicit "mark
+ // unread") so a stale readAt can't linger on a row that's unread again.
+ @Query("UPDATE articles SET isRead = :read, readAt = :readAt WHERE id = :id")
+ suspend fun setRead(id: String, read: Boolean, readAt: Long?)
// Explicit bulk action ("mark read from here") -- batched per confirmed tap,
// never called implicitly (e.g. by scrolling).
- @Query("UPDATE articles SET isRead = 1 WHERE id IN (:ids)")
- suspend fun markReadBatch(ids: List<String>)
+ @Query("UPDATE articles SET isRead = 1, readAt = :readAt WHERE id IN (:ids)")
+ suspend fun markReadBatch(ids: List<String>, readAt: Long)
- @Query("UPDATE articles SET isRead = 1 WHERE feedId = :feedId")
- suspend fun markAllReadForFeed(feedId: String)
+ @Query("UPDATE articles SET isRead = 1, readAt = :readAt WHERE feedId = :feedId")
+ suspend fun markAllReadForFeed(feedId: String, readAt: Long)
// Run when unsubscribing from a feed. Saved articles are deliberately kept
// regardless (isSaved is a separate, durable "keep forever" concept,
@@ -196,8 +209,8 @@ interface ArticleDao {
// and SQLite has a hard per-statement limit on bound variables (historically 999 on many
// Android SQLite builds). Chunking well under any realistic limit means this can't throw
// "too many SQL variables" and crash on an otherwise routine bulk mark-read tap.
-suspend fun ArticleDao.markReadBatchChunked(ids: List<String>) {
- ids.chunked(500).forEach { chunk -> markReadBatch(chunk) }
+suspend fun ArticleDao.markReadBatchChunked(ids: List<String>, readAt: Long = System.currentTimeMillis()) {
+ ids.chunked(500).forEach { chunk -> markReadBatch(chunk, readAt) }
}
@Dao