From eeedee5555dbe4cc1c2c3251ae2e9c69f91d419b Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Tue, 11 Jul 2023 16:43:04 -0300
Subject: [PATCH] Mark blog posts read when visible for some time

---
 .../briar/desktop/blog/BlogPostItem.kt        |  4 ++-
 .../briar/desktop/blog/BlogScreen.kt          |  7 +++-
 .../briar/desktop/blog/FeedScreen.kt          | 11 ++++--
 .../briar/desktop/blog/FeedViewModel.kt       | 35 ++++++++++++++++++-
 4 files changed, 51 insertions(+), 6 deletions(-)

diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/BlogPostItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/BlogPostItem.kt
index 2c4fb589b7..3596fd7a38 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/BlogPostItem.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/BlogPostItem.kt
@@ -30,7 +30,7 @@ sealed class BlogPost(
     open val text: String?,
 ) : Comparable<BlogPost> {
     abstract val postHeader: BlogPostHeader
-    val isRead: Boolean get() = header.isRead
+    abstract val isRead: Boolean
     val id: MessageId get() = header.id
     val groupId: GroupId get() = header.groupId
     val timestamp: Long get() = header.timestamp
@@ -46,6 +46,7 @@ sealed class BlogPost(
 data class BlogPostItem(
     override val header: BlogPostHeader,
     override val text: String,
+    override val isRead: Boolean = header.isRead,
 ) : BlogPost(header, text) {
     override val postHeader: BlogPostHeader get() = header
 }
@@ -54,6 +55,7 @@ data class BlogCommentItem(
     override val header: BlogCommentHeader,
     override val postHeader: BlogPostHeader,
     override val text: String?,
+    override val isRead: Boolean = header.isRead,
 ) : BlogPost(header, text) {
 
     companion object {
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/BlogScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/BlogScreen.kt
index 657b084b14..f50ec84823 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/BlogScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/BlogScreen.kt
@@ -51,7 +51,12 @@ fun BlogScreen(viewModel: FeedViewModel = viewModel()) {
                         Text(i18n("blog.empty.state"), Modifier.padding(16.dp))
                     }
                 } else {
-                    FeedScreen(viewModel.posts, viewModel::selectPost, Modifier.padding(padding))
+                    FeedScreen(
+                        posts = viewModel.posts,
+                        onItemSelected = viewModel::selectPost,
+                        onBlogPostsVisible = viewModel::onPostsVisible,
+                        modifier = Modifier.padding(padding)
+                    )
                 }
             }
         },
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/FeedScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/FeedScreen.kt
index 629ead9061..832dce0980 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/FeedScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/FeedScreen.kt
@@ -54,11 +54,16 @@ fun main() = preview(
             add(getRandomBlogPost(getStringParameter("text"), getLongParameter("timestamp")))
         }
     }
-    FeedScreen(items, {})
+    FeedScreen(items, {}, {})
 }
 
 @Composable
-fun FeedScreen(posts: List<BlogPost>, onItemSelected: (BlogPost) -> Unit, modifier: Modifier = Modifier) {
+fun FeedScreen(
+    posts: List<BlogPost>,
+    onItemSelected: (BlogPost) -> Unit,
+    onBlogPostsVisible: (List<MessageId>) -> Unit,
+    modifier: Modifier = Modifier,
+) {
     val scrollState = rememberLazyListState()
     // scroll to first unread item if needed
     val lastUnreadIndex = posts.indexOfLast { item -> !item.isRead }
@@ -101,7 +106,7 @@ fun FeedScreen(posts: List<BlogPost>, onItemSelected: (BlogPost) -> Unit, modifi
                 val visibleMessageIds = scrollState.layoutInfo.reallyVisibleItemsInfo.map {
                     it.key as MessageId
                 }
-                // TODO onBlogPostsVisible(visibleMessageIds)
+                onBlogPostsVisible(visibleMessageIds)
             }
         }
     }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/FeedViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/FeedViewModel.kt
index 99ff47bd5d..fb5978fda5 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/FeedViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/FeedViewModel.kt
@@ -38,6 +38,7 @@ import org.briarproject.briar.api.blog.BlogPostHeader
 import org.briarproject.briar.api.blog.event.BlogPostAddedEvent
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.threading.UiExecutor
+import org.briarproject.briar.desktop.utils.replaceIf
 import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
 import org.briarproject.briar.desktop.viewmodel.asList
 import org.briarproject.briar.desktop.viewmodel.asState
@@ -51,7 +52,7 @@ class FeedViewModel @Inject constructor(
     briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
-    eventBus: EventBus,
+    private val eventBus: EventBus,
 ) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) {
 
     companion object {
@@ -169,4 +170,36 @@ class FeedViewModel @Inject constructor(
     private fun getPostText(txn: Transaction, m: MessageId): String {
         return HtmlUtils.cleanArticle(blogManager.getPostText(txn, m))
     }
+
+    @UiExecutor
+    fun onPostsVisible(messageIds: List<MessageId>) = markBlogPostRead { item ->
+        messageIds.contains(item.id)
+    }
+
+    /**
+     * Marks the [BlogPost]s as read for those the given [predicate] returns true.
+     */
+    @UiExecutor
+    private fun markBlogPostRead(predicate: (BlogPost) -> Boolean) {
+        val readIds = posts.filter { item ->
+            predicate(item) && !item.isRead
+        }.map { item ->
+            item.id
+        }
+        if (readIds.isNotEmpty()) {
+            runOnDbThread {
+                readIds.forEach { id ->
+                    blogManager.setReadFlag(id, false)
+                }
+                // TODO introduce Transaction for BlogManager#setReadFlag() and attach event there
+//                eventBus.broadcast()
+            }
+            _posts.replaceIf({ it.id in readIds }) {
+                when (it) {
+                    is BlogPostItem -> it.copy(isRead = true)
+                    is BlogCommentItem -> it.copy(isRead = true)
+                }
+            }
+        }
+    }
 }
-- 
GitLab