From aaf032332b5476c6ad3673cf2113c7676863da80 Mon Sep 17 00:00:00 2001
From: Ash <glaxyinfinite@outlook.com>
Date: Fri, 8 Apr 2022 04:35:28 +0800
Subject: [PATCH] Add mark all as read feature

---
 .../java/me/ash/reader/data/dao/ArticleDao.kt | 76 +++++++++------
 .../data/repository/AbstractRssRepository.kt  |  8 ++
 .../data/repository/LocalRssRepository.kt     | 34 +++++++
 .../ash/reader/ui/component/AnimatedPopup.kt  | 48 ++++++++++
 .../me/ash/reader/ui/component/DisplayText.kt | 12 ++-
 .../java/me/ash/reader/ui/ext/ModifierExt.kt  |  8 +-
 .../ash/reader/ui/page/home/feeds/FeedItem.kt |  2 +-
 .../reader/ui/page/home/feeds/FeedsPage.kt    |  3 -
 .../ash/reader/ui/page/home/flow/FlowPage.kt  | 58 +++++-------
 .../reader/ui/page/home/flow/FlowViewModel.kt | 59 ++++++++++--
 .../reader/ui/page/home/flow/MarkAsReadBar.kt | 94 ++++++++++++++-----
 .../ash/reader/ui/page/home/read/ReadPage.kt  |  3 -
 .../reader/ui/page/home/read/ReadViewModel.kt | 12 ++-
 .../reader/ui/page/settings/SettingsPage.kt   |  1 -
 14 files changed, 304 insertions(+), 114 deletions(-)
 create mode 100644 app/src/main/java/me/ash/reader/ui/component/AnimatedPopup.kt

diff --git a/app/src/main/java/me/ash/reader/data/dao/ArticleDao.kt b/app/src/main/java/me/ash/reader/data/dao/ArticleDao.kt
index 4509c3ad..14113519 100644
--- a/app/src/main/java/me/ash/reader/data/dao/ArticleDao.kt
+++ b/app/src/main/java/me/ash/reader/data/dao/ArticleDao.kt
@@ -6,46 +6,68 @@ import kotlinx.coroutines.flow.Flow
 import me.ash.reader.data.entity.Article
 import me.ash.reader.data.entity.ArticleWithFeed
 import me.ash.reader.data.entity.ImportantCount
+import java.util.*
 
 @Dao
 interface ArticleDao {
     @Query(
         """
-        UPDATE article SET isUnread = 0 
+        UPDATE article SET isUnread = :isUnread 
         WHERE accountId = :accountId
-        AND isUnread = 1
-        AND date <= :before
+        AND date < :before
         """
     )
-    suspend fun markAllAsRead(accountId: Int, before: Long)
+    suspend fun markAllAsRead(
+        accountId: Int,
+        isUnread: Boolean,
+        before: Date,
+    )
 
     @Query(
         """
-        UPDATE article SET isUnread = 0 
-        WHERE accountId = :accountId
-        AND isUnread = 1
-        AND date <= :before
-        AND feedId = :feedId
+        UPDATE article SET isUnread = :isUnread 
+        WHERE feedId IN (
+            SELECT id FROM feed 
+            WHERE groupId = :groupId
+        )
+        AND accountId = :accountId
+        AND date < :before
         """
     )
-    suspend fun markAllAsReadByFeedId(accountId: Int, before: Long, feedId: String)
-//
-//    @Query(
-//        """
-//        UPDATE article SET isUnread = 0
-//        WHERE accountId = :accountId
-//        AND isUnread = 1
-//        AND date <= :before
-//        AND feedId = :feedId
-//
-//        SELECT * FROM `group` AS a, feed AS b, article AS c
-//        WHERE a.accountId = :accountId
-//        AND a.id = b.groupId
-//        AND b.groupId = :groupId
-//        AND c.feedId = b.id
-//        """
-//    )
-//    suspend fun markAllAsReadByGroupId(accountId: Int, before: Long, groupId: String)
+    suspend fun markAllAsReadByGroupId(
+        accountId: Int,
+        groupId: String,
+        isUnread: Boolean,
+        before: Date,
+    )
+
+    @Query(
+        """
+        UPDATE article SET isUnread = :isUnread 
+        WHERE feedId = :feedId
+        AND accountId = :accountId
+        AND date < :before
+        """
+    )
+    suspend fun markAllAsReadByFeedId(
+        accountId: Int,
+        feedId: String,
+        isUnread: Boolean,
+        before: Date,
+    )
+
+    @Query(
+        """
+        UPDATE article SET isUnread = :isUnread 
+        WHERE id = :articleId
+        AND accountId = :accountId
+        """
+    )
+    suspend fun markAsReadByArticleId(
+        accountId: Int,
+        articleId: String,
+        isUnread: Boolean,
+    )
 
     @Query(
         """
diff --git a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt
index 6052dd8c..f28e01f8 100644
--- a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt
+++ b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt
@@ -38,6 +38,14 @@ abstract class AbstractRssRepository constructor(
 
     abstract suspend fun sync(coroutineWorker: CoroutineWorker): ListenableWorker.Result
 
+    abstract suspend fun markAsRead(
+        groupId: String?,
+        feedId: String?,
+        articleId: String?,
+        before: Date?,
+        isUnread: Boolean,
+    )
+
     fun doSync() {
         workManager.enqueueUniquePeriodicWork(
             SyncWorker.WORK_NAME,
diff --git a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt
index a1ffb916..5aa01231 100644
--- a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt
+++ b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt
@@ -125,6 +125,40 @@ class LocalRssRepository @Inject constructor(
         }
     }
 
+    override suspend fun markAsRead(
+        groupId: String?,
+        feedId: String?,
+        articleId: String?,
+        before: Date?,
+        isUnread: Boolean,
+    ) {
+        val accountId = context.currentAccountId
+        when {
+            groupId != null -> {
+                articleDao.markAllAsReadByGroupId(
+                    accountId = accountId,
+                    groupId = groupId,
+                    isUnread = isUnread,
+                    before = before ?: Date(Long.MAX_VALUE)
+                )
+            }
+            feedId != null -> {
+                articleDao.markAllAsReadByFeedId(
+                    accountId = accountId,
+                    feedId = feedId,
+                    isUnread = isUnread,
+                    before = before ?: Date(Long.MAX_VALUE)
+                )
+            }
+            articleId != null -> {
+                articleDao.markAsReadByArticleId(accountId, articleId, isUnread)
+            }
+            else -> {
+                articleDao.markAllAsRead(accountId, isUnread, before ?: Date(Long.MAX_VALUE))
+            }
+        }
+    }
+
     data class ArticleNotify(
         val articles: List<Article>,
         val isNotify: Boolean,
diff --git a/app/src/main/java/me/ash/reader/ui/component/AnimatedPopup.kt b/app/src/main/java/me/ash/reader/ui/component/AnimatedPopup.kt
new file mode 100644
index 00000000..6345a036
--- /dev/null
+++ b/app/src/main/java/me/ash/reader/ui/component/AnimatedPopup.kt
@@ -0,0 +1,48 @@
+package me.ash.reader.ui.component
+
+import androidx.compose.animation.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.*
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
+import com.google.accompanist.insets.LocalWindowInsets
+
+@Composable
+fun AnimatedPopup(
+    visible: Boolean = false,
+    absoluteY: Dp = Dp.Hairline,
+    absoluteX: Dp = Dp.Hairline,
+    onDismissRequest: () -> Unit = {},
+    content: @Composable () -> Unit = {},
+) {
+    val density = LocalDensity.current
+    val insets = LocalWindowInsets.current
+
+    Popup(
+        properties = PopupProperties(focusable = visible),
+        onDismissRequest = onDismissRequest,
+        popupPositionProvider = object : PopupPositionProvider {
+            override fun calculatePosition(
+                anchorBounds: IntRect,
+                windowSize: IntSize,
+                layoutDirection: LayoutDirection,
+                popupContentSize: IntSize
+            ): IntOffset {
+                return IntOffset(
+                    x = with(density) { (absoluteX).roundToPx() },
+                    y = with(density) { (absoluteY).roundToPx() + insets.statusBars.top }
+                )
+            }
+        },
+    ) {
+        AnimatedVisibility(
+            visible = visible,
+            enter = fadeIn() + expandVertically(),
+            exit = fadeOut() + shrinkVertically(),
+        ) {
+            content()
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt b/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt
index 748ffcdb..1c64a020 100644
--- a/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt
+++ b/app/src/main/java/me/ash/reader/ui/component/DisplayText.kt
@@ -3,11 +3,13 @@ package me.ash.reader.ui.component
 import androidx.compose.animation.*
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.BaselineShift
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 
@@ -28,8 +30,11 @@ fun DisplayText(
             )
     ) {
         Text(
+            modifier = Modifier.height(44.dp),
             text = text,
-            style = MaterialTheme.typography.displaySmall,
+            style = MaterialTheme.typography.displaySmall.copy(
+                baselineShift = BaselineShift.Superscript
+            ),
             color = MaterialTheme.colorScheme.onSurface,
             maxLines = 1,
             overflow = TextOverflow.Ellipsis,
@@ -40,8 +45,11 @@ fun DisplayText(
             exit = fadeOut() + shrinkVertically(),
         ) {
             Text(
+                modifier = Modifier.height(16.dp),
                 text = desc,
-                style = MaterialTheme.typography.labelMedium,
+                style = MaterialTheme.typography.labelMedium.copy(
+                    baselineShift = BaselineShift.Superscript
+                ),
                 color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
                 maxLines = 1,
                 overflow = TextOverflow.Ellipsis,
diff --git a/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt b/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt
index 6d86d305..14931de5 100644
--- a/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt
+++ b/app/src/main/java/me/ash/reader/ui/ext/ModifierExt.kt
@@ -70,28 +70,30 @@ fun Modifier.combinedFeedbackClickable(
     isSound: Boolean? = false,
     onPressDown: (() -> Unit)? = null,
     onPressUp: (() -> Unit)? = null,
+    onTap: (() -> Unit)? = null,
     onLongClick: (() -> Unit)? = null,
     onDoubleClick: (() -> Unit)? = null,
     onClick: (() -> Unit)? = null,
 ): Modifier {
     val view = LocalView.current
     val interactionSource = remember { MutableInteractionSource() }
-    return if (onPressDown != null || onPressUp != null) {
+    return if (onPressDown != null || onPressUp != null || onTap != null) {
         indication(interactionSource, LocalIndication.current)
             .pointerInput(Unit) {
                 detectTapGestures(
                     onPress = { offset ->
                         onPressDown?.let {
+                            it()
                             if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
                             val press = PressInteraction.Press(offset)
                             interactionSource.emit(press)
                             tryAwaitRelease()
+                            onPressUp?.invoke()
                             interactionSource.emit(PressInteraction.Release(press))
-                            it()
                         }
                     },
                     onTap = {
-                        onPressUp?.let {
+                        onTap?.let {
                             if (isHaptic == true) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
                             if (isSound == true) view.playSoundEffect(SoundEffectConstants.CLICK)
                             it()
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt
index 7270d36d..dd04af7b 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt
@@ -64,7 +64,7 @@ fun FeedItem(
                     modifier = Modifier
                         .size(20.dp)
                         .clip(CircleShape)
-                        .background(MaterialTheme.colorScheme.outline),
+                        .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
                 ) {}
                 Text(
                     modifier = Modifier.padding(start = 12.dp, end = 6.dp),
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt
index 1fa24c47..2c0798bc 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt
@@ -109,7 +109,6 @@ fun FeedsPage(
                 title = {},
                 navigationIcon = {
                     FeedbackIconButton(
-                        isHaptic = false,
                         modifier = Modifier.size(20.dp),
                         imageVector = Icons.Outlined.Settings,
                         contentDescription = stringResource(R.string.settings),
@@ -120,7 +119,6 @@ fun FeedsPage(
                 },
                 actions = {
                     FeedbackIconButton(
-                        isHaptic = false,
                         modifier = Modifier.rotate(if (isSyncing) angle else 0f),
                         imageVector = Icons.Rounded.Refresh,
                         contentDescription = stringResource(R.string.refresh),
@@ -131,7 +129,6 @@ fun FeedsPage(
                         }
                     }
                     FeedbackIconButton(
-                        isHaptic = false,
                         imageVector = Icons.Rounded.Add,
                         contentDescription = stringResource(R.string.subscribe),
                         tint = MaterialTheme.colorScheme.onSurface,
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt
index a3978944..51e97fc6 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt
@@ -3,8 +3,10 @@ package me.ash.reader.ui.page.home.flow
 import androidx.compose.animation.*
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.rounded.ArrowBack
@@ -16,7 +18,6 @@ import androidx.compose.material3.Scaffold
 import androidx.compose.material3.SmallTopAppBar
 import androidx.compose.runtime.*
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.res.stringResource
@@ -71,28 +72,13 @@ fun FlowPage(
         )
     }
 
-//    LaunchedEffect(viewState.listState.isScrollInProgress) {
-//        Log.i("RLog", "isScrollInProgress: ${viewState.listState.isScrollInProgress}")
-//        if (viewState.listState.isScrollInProgress) {
-//            Log.i("RLog", "isScrollInProgress: ${true}")
-//            markAsRead = false
-//        }
-//    }
-
     Scaffold(
-        modifier = Modifier
-            .background(MaterialTheme.colorScheme.surface)
-            .pointerInput(markAsRead) {
-                detectTapGestures {
-                    markAsRead = false
-                }
-            },
+        modifier = Modifier.background(MaterialTheme.colorScheme.surface),
         topBar = {
             SmallTopAppBar(
                 title = {},
                 navigationIcon = {
                     FeedbackIconButton(
-                        isHaptic = false,
                         imageVector = Icons.Rounded.ArrowBack,
                         contentDescription = stringResource(R.string.back),
                         tint = MaterialTheme.colorScheme.onSurface
@@ -107,7 +93,6 @@ fun FlowPage(
                         exit = fadeOut() + shrinkVertically(),
                     ) {
                         FeedbackIconButton(
-                            isHaptic = false,
                             imageVector = Icons.Rounded.DoneAll,
                             contentDescription = stringResource(R.string.mark_all_as_read),
                             tint = if (markAsRead) {
@@ -123,7 +108,6 @@ fun FlowPage(
                         }
                     }
                     FeedbackIconButton(
-                        isHaptic = false,
                         imageVector = Icons.Rounded.Search,
                         contentDescription = stringResource(R.string.search),
                         tint = MaterialTheme.colorScheme.onSurface,
@@ -162,17 +146,30 @@ fun FlowPage(
                             enter = fadeIn() + expandVertically(),
                             exit = fadeOut() + shrinkVertically(),
                         ) {
-                            Column {
-                                MarkAsReadBar()
-                                Spacer(modifier = Modifier.height(24.dp))
-                            }
+                            Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
+                        }
+                        MarkAsReadBar(
+                            visible = markAsRead,
+                            absoluteY = if (isSyncing) (4 + 16 + 180).dp else 180.dp,
+                            onDismissRequest = {
+                                markAsRead = false
+                            },
+                        ) {
+                            markAsRead = false
+                            flowViewModel.dispatch(
+                                FlowViewAction.MarkAsRead(
+                                    groupId = filterState.group?.id,
+                                    feedId = filterState.feed?.id,
+                                    articleId = null,
+                                    markAsReadBefore = it,
+                                )
+                            )
                         }
                     }
                     generateArticleList(
                         context = context,
                         pagingItems = pagingItems,
                     ) {
-                        markAsRead = false
                         onItemClick(it)
                     }
                     item {
@@ -190,14 +187,7 @@ fun FlowPage(
                     .height(60.dp)
                     .fillMaxWidth(),
                 filter = filterState.filter,
-                filterOnClick = {
-                    markAsRead = false
-                    onFilterChange(
-                        filterState.copy(
-                            filter = it
-                        )
-                    )
-                },
+                filterOnClick = { onFilterChange(filterState.copy(filter = it)) },
             )
         }
     )
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt
index a41f8bd3..be5720b4 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt
@@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
 import me.ash.reader.data.entity.ArticleWithFeed
 import me.ash.reader.data.repository.RssRepository
 import me.ash.reader.ui.page.home.FilterState
+import java.util.*
 import javax.inject.Inject
 
 @HiltViewModel
@@ -28,14 +29,11 @@ class FlowViewModel @Inject constructor(
             is FlowViewAction.FetchData -> fetchData(action.filterState)
             is FlowViewAction.ChangeRefreshing -> changeRefreshing(action.isRefreshing)
             is FlowViewAction.ScrollToItem -> scrollToItem(action.index)
-            is FlowViewAction.PeekSyncWork -> peekSyncWork()
-        }
-    }
-
-    private fun peekSyncWork() {
-        _viewState.update {
-            it.copy(
-                syncWorkInfo = rssRepository.get().peekWork()
+            is FlowViewAction.MarkAsRead -> markAsRead(
+                action.groupId,
+                action.feedId,
+                action.articleId,
+                action.markAsReadBefore,
             )
         }
     }
@@ -76,6 +74,37 @@ class FlowViewModel @Inject constructor(
             it.copy(isRefreshing = isRefreshing)
         }
     }
+
+    private fun markAsRead(
+        groupId: String?,
+        feedId: String?,
+        articleId: String?,
+        markAsReadBefore: MarkAsReadBefore
+    ) {
+        viewModelScope.launch {
+            rssRepository.get().markAsRead(
+                groupId = groupId,
+                feedId = feedId,
+                articleId = articleId,
+                before = when (markAsReadBefore) {
+                    MarkAsReadBefore.All -> null
+                    MarkAsReadBefore.OneDay -> Calendar.getInstance().apply {
+                        time = Date()
+                        add(Calendar.DAY_OF_MONTH, -1)
+                    }.time
+                    MarkAsReadBefore.ThreeDays -> Calendar.getInstance().apply {
+                        time = Date()
+                        add(Calendar.DAY_OF_MONTH, -3)
+                    }.time
+                    MarkAsReadBefore.SevenDays -> Calendar.getInstance().apply {
+                        time = Date()
+                        add(Calendar.DAY_OF_MONTH, -7)
+                    }.time
+                },
+                isUnread = false,
+            )
+        }
+    }
 }
 
 data class ArticleViewState(
@@ -99,5 +128,17 @@ sealed class FlowViewAction {
         val index: Int
     ) : FlowViewAction()
 
-    object PeekSyncWork : FlowViewAction()
+    data class MarkAsRead(
+        val groupId: String?,
+        val feedId: String?,
+        val articleId: String?,
+        val markAsReadBefore: MarkAsReadBefore
+    ) : FlowViewAction()
+}
+
+enum class MarkAsReadBefore {
+    SevenDays,
+    ThreeDays,
+    OneDay,
+    All,
 }
\ No newline at end of file
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt
index c6807533..39cd16cb 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/MarkAsReadBar.kt
@@ -1,5 +1,10 @@
 package me.ash.reader.ui.page.home.flow
 
+import android.view.HapticFeedbackConstants
+import android.view.SoundEffectConstants
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -7,38 +12,68 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import me.ash.reader.R
+import me.ash.reader.ui.component.AnimatedPopup
 
 @Composable
-fun MarkAsReadBar() {
-    Row(
-        modifier = Modifier
-            .padding(horizontal = 24.dp)
-            .fillMaxWidth(),
-        verticalAlignment = Alignment.CenterVertically,
+fun MarkAsReadBar(
+    visible: Boolean = false,
+    absoluteY: Dp = Dp.Hairline,
+    onDismissRequest: () -> Unit = {},
+    onItemClick: (MarkAsReadBefore) -> Unit = {},
+) {
+    val animated = remember { Animatable(absoluteY.value) }
+
+    LaunchedEffect(absoluteY) {
+        animated.animateTo(absoluteY.value, spring(stiffness = Spring.StiffnessMediumLow))
+    }
+
+    AnimatedPopup(
+        visible = visible,
+        absoluteY = animated.value.dp,
+        onDismissRequest = onDismissRequest,
     ) {
-        MarkAsReadBarItem(
-            modifier = Modifier.weight(1f),
-            text = stringResource(R.string.seven_days),
-        )
-        MarkAsReadBarItem(
-            modifier = Modifier.weight(1f),
-            text = stringResource(R.string.three_days),
-        )
-        MarkAsReadBarItem(
-            modifier = Modifier.weight(1f),
-            text = stringResource(R.string.one_day),
-        )
-        MarkAsReadBarItem(
-            modifier = Modifier.weight(2.5f),
-            text = stringResource(R.string.mark_all_as_read),
-            isPrimary = true,
-        )
+        Row(
+            modifier = Modifier
+                .padding(horizontal = 24.dp)
+                .fillMaxWidth(),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            MarkAsReadBarItem(
+                modifier = Modifier.width(56.dp),
+                text = stringResource(R.string.seven_days),
+            ) {
+                onItemClick(MarkAsReadBefore.SevenDays)
+            }
+            MarkAsReadBarItem(
+                modifier = Modifier.width(56.dp),
+                text = stringResource(R.string.three_days),
+            ) {
+                onItemClick(MarkAsReadBefore.ThreeDays)
+            }
+            MarkAsReadBarItem(
+                modifier = Modifier.width(56.dp),
+                text = stringResource(R.string.one_day),
+            ) {
+                onItemClick(MarkAsReadBefore.OneDay)
+            }
+            MarkAsReadBarItem(
+                modifier = Modifier.weight(1f),
+                text = stringResource(R.string.mark_all_as_read),
+                isPrimary = true,
+            ) {
+                onItemClick(MarkAsReadBefore.All)
+            }
+        }
     }
 }
 
@@ -47,12 +82,19 @@ fun MarkAsReadBarItem(
     modifier: Modifier = Modifier,
     text: String,
     isPrimary: Boolean = false,
+    onClick: () -> Unit = {},
 ) {
+    val view = LocalView.current
+
     Surface(
         modifier = modifier
-            .height(52.dp)
+            .height(56.dp)
             .clip(RoundedCornerShape(16.dp))
-            .clickable { },
+            .clickable {
+                view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
+                view.playSoundEffect(SoundEffectConstants.CLICK)
+                onClick()
+            },
         tonalElevation = 2.dp,
         shape = RoundedCornerShape(16.dp),
         color = if (isPrimary) {
@@ -70,7 +112,7 @@ fun MarkAsReadBarItem(
                 text = text,
                 style = MaterialTheme.typography.titleSmall,
                 color = if (isPrimary) {
-                    MaterialTheme.colorScheme.onPrimaryContainer
+                    MaterialTheme.colorScheme.onSurface
                 } else {
                     MaterialTheme.colorScheme.secondary
                 },
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt
index 9578dab6..ea003f59 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt
@@ -143,7 +143,6 @@ private fun TopBar(
             title = {},
             navigationIcon = {
                 FeedbackIconButton(
-                    isHaptic = false,
                     imageVector = Icons.Rounded.Close,
                     contentDescription = stringResource(R.string.close),
                     tint = MaterialTheme.colorScheme.onSurface
@@ -156,7 +155,6 @@ private fun TopBar(
             actions = {
                 if (isShowActions) {
                     FeedbackIconButton(
-                        isHaptic = false,
                         modifier = Modifier.size(22.dp),
                         imageVector = Icons.Outlined.Headphones,
                         contentDescription = stringResource(R.string.mark_all_as_read),
@@ -164,7 +162,6 @@ private fun TopBar(
                     ) {
                     }
                     FeedbackIconButton(
-                        isHaptic = false,
                         imageVector = Icons.Outlined.MoreVert,
                         contentDescription = stringResource(R.string.search),
                         tint = MaterialTheme.colorScheme.onSurface,
diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt
index a9497053..9d0aa4bb 100644
--- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadViewModel.kt
@@ -79,7 +79,7 @@ class ReadViewModel @Inject constructor(
 
     private fun markUnread(isUnread: Boolean) {
         val articleWithFeed = _viewState.value.articleWithFeed ?: return
-        viewModelScope.launch(Dispatchers.IO) {
+        viewModelScope.launch {
             _viewState.update {
                 it.copy(
                     articleWithFeed = articleWithFeed.copy(
@@ -89,10 +89,12 @@ class ReadViewModel @Inject constructor(
                     )
                 )
             }
-            rssRepository.get().updateArticleInfo(
-                articleWithFeed.article.copy(
-                    isUnread = isUnread
-                )
+            rssRepository.get().markAsRead(
+                groupId = null,
+                feedId = null,
+                articleId = _viewState.value.articleWithFeed!!.article.id,
+                before = null,
+                isUnread = isUnread,
             )
         }
     }
diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt
index 06209db8..770b7cf9 100644
--- a/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt
+++ b/app/src/main/java/me/ash/reader/ui/page/settings/SettingsPage.kt
@@ -33,7 +33,6 @@ fun SettingsPage(
                 title = {},
                 navigationIcon = {
                     FeedbackIconButton(
-                        isHaptic = false,
                         imageVector = Icons.Rounded.ArrowBack,
                         contentDescription = stringResource(R.string.back),
                         tint = MaterialTheme.colorScheme.onSurface