From 01f5f3865b9ec3337ac92c034ca6a00154b3f72d Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 22 Jan 2024 22:08:20 +0800 Subject: [PATCH] fix(greader): mark as read --- .../reader/domain/repository/ArticleDao.kt | 67 ++++++++++++++- .../ash/reader/domain/repository/FeedDao.kt | 15 +++- .../domain/service/AbstractRssRepository.kt | 13 ++- .../reader/domain/service/FeverRssService.kt | 19 +++-- .../domain/service/GoogleReaderRssService.kt | 83 ++++++++++--------- .../reader/infrastructure/rss/RssHelper.kt | 18 ++-- .../rss/provider/greader/GoogleReaderAPI.kt | 1 + .../me/ash/reader/ui/component/FeedIcon.kt | 2 +- .../java/me/ash/reader/ui/ext/StringExt.kt | 3 + .../reader/ui/page/home/flow/FlowViewModel.kt | 2 +- .../ui/page/home/reading/ReadingViewModel.kt | 19 +++-- 11 files changed, 166 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/me/ash/reader/domain/repository/ArticleDao.kt b/app/src/main/java/me/ash/reader/domain/repository/ArticleDao.kt index 4885512a0..00a9b4ccb 100644 --- a/app/src/main/java/me/ash/reader/domain/repository/ArticleDao.kt +++ b/app/src/main/java/me/ash/reader/domain/repository/ArticleDao.kt @@ -562,9 +562,70 @@ interface ArticleDao { ORDER BY date DESC """ ) - fun queryArticleMetadataAll( - accountId: Int, - ): List + fun queryMetadataAll(accountId: Int): List + + @Transaction + @Query( + """ + SELECT id, isUnread, isStarred FROM article + WHERE accountId = :accountId + AND date < :before + ORDER BY date DESC + """ + ) + fun queryMetadataAll(accountId: Int, before: Date): List + + @Transaction + @Query( + """ + SELECT id, isUnread, isStarred FROM article + WHERE accountId = :accountId + AND feedId = :feedId + ORDER BY date DESC + """ + ) + fun queryMetadataByFeedId(accountId: Int, feedId: String): List + + @Transaction + @Query( + """ + SELECT id, isUnread, isStarred FROM article + WHERE accountId = :accountId + AND feedId = :feedId + AND date < :before + ORDER BY date DESC + """ + ) + fun queryMetadataByFeedId(accountId: Int, feedId: String, before: Date): List + + @Transaction + @Query( + """ + SELECT a.id, a.isUnread, a.isStarred + FROM article AS a + LEFT JOIN feed AS b ON b.id = a.feedId + LEFT JOIN `group` AS c ON c.id = b.groupId + WHERE c.id = :groupId + AND a.accountId = :accountId + ORDER BY a.date DESC + """ + ) + fun queryMetadataByGroupId(accountId: Int, groupId: String): List + + @Transaction + @Query( + """ + SELECT a.id, a.isUnread, a.isStarred + FROM article AS a + LEFT JOIN feed AS b ON b.id = a.feedId + LEFT JOIN `group` AS c ON c.id = b.groupId + WHERE c.id = :groupId + AND a.accountId = :accountId + AND a.date < :before + ORDER BY a.date DESC + """ + ) + fun queryMetadataByGroupId(accountId: Int, groupId: String, before: Date): List @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(vararg article: Article) diff --git a/app/src/main/java/me/ash/reader/domain/repository/FeedDao.kt b/app/src/main/java/me/ash/reader/domain/repository/FeedDao.kt index 8b004c640..fd1e0dd14 100644 --- a/app/src/main/java/me/ash/reader/domain/repository/FeedDao.kt +++ b/app/src/main/java/me/ash/reader/domain/repository/FeedDao.kt @@ -1,5 +1,6 @@ package me.ash.reader.domain.repository +import android.util.Log import androidx.room.* import me.ash.reader.domain.model.feed.Feed @@ -91,7 +92,16 @@ interface FeedDao { """ SELECT * FROM feed WHERE accountId = :accountId - and url = :url + AND (icon IS NUll OR icon = '') + """ + ) + suspend fun queryNoIcon(accountId: Int): List + + @Query( + """ + SELECT * FROM feed + WHERE accountId = :accountId + AND url = :url """ ) suspend fun queryByLink(accountId: Int, url: String): List @@ -114,6 +124,9 @@ interface FeedDao { if (feed == null) { insert(it) } else { + Log.i("RLog", "insertOrUpdate it: $it") + Log.i("RLog", "insertOrUpdate feed: $feed") + if (it.icon.isNullOrEmpty()) it.icon = feed.icon // TODO: Consider migrating the fields to be nullable. it.isNotification = feed.isNotification it.isFullContent = feed.isFullContent diff --git a/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt index f4ba9895d..414b1360d 100644 --- a/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt +++ b/app/src/main/java/me/ash/reader/domain/service/AbstractRssRepository.kt @@ -29,6 +29,7 @@ import me.ash.reader.infrastructure.preference.KeepArchivedPreference import me.ash.reader.infrastructure.preference.SyncIntervalPreference import me.ash.reader.infrastructure.rss.RssHelper import me.ash.reader.ui.ext.currentAccountId +import me.ash.reader.ui.ext.decodeHTML import me.ash.reader.ui.ext.spacerDollar import java.util.* @@ -61,7 +62,7 @@ abstract class AbstractRssRepository( val accountId = context.currentAccountId val feed = Feed( id = accountId.spacerDollar(UUID.randomUUID().toString()), - name = searchedFeed.title!!, + name = searchedFeed.title.decodeHTML()!!, url = feedLink, groupId = groupId, accountId = accountId, @@ -166,13 +167,9 @@ abstract class AbstractRssRepository( val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id) val articles = rssHelper.queryRssXml(feed, latest?.link) if (feed.icon == null) { - try { - val iconLink = rssHelper.queryRssIconLink(feed.url) - if (iconLink != null) { - rssHelper.saveRssIcon(feedDao, feed, iconLink) - } - } catch (e: Exception) { - Log.i("RLog", "queryRssIcon is failed: ${e.message}") + val iconLink = rssHelper.queryRssIconLink(feed.url) + if (iconLink != null) { + rssHelper.saveRssIcon(feedDao, feed, iconLink) } } return FeedWithArticle( diff --git a/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt b/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt index 397ba5417..a2bf84fd2 100644 --- a/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt +++ b/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt @@ -1,7 +1,6 @@ package me.ash.reader.domain.service import android.content.Context -import android.text.Html import android.util.Log import androidx.work.CoroutineWorker import androidx.work.ListenableWorker @@ -29,10 +28,7 @@ import me.ash.reader.infrastructure.di.MainDispatcher import me.ash.reader.infrastructure.rss.RssHelper import me.ash.reader.infrastructure.rss.provider.fever.FeverAPI import me.ash.reader.infrastructure.rss.provider.fever.FeverDTO -import me.ash.reader.ui.ext.currentAccountId -import me.ash.reader.ui.ext.dollarLast -import me.ash.reader.ui.ext.showToast -import me.ash.reader.ui.ext.spacerDollar +import me.ash.reader.ui.ext.* import net.dankito.readability4j.extended.Readability4JExtended import java.util.* import javax.inject.Inject @@ -177,7 +173,7 @@ class FeverRssService @Inject constructor( feedsBody.feeds?.map { Feed( id = accountId.spacerDollar(it.id!!), - name = it.title ?: context.getString(R.string.empty), + name = it.title.decodeHTML() ?: context.getString(R.string.empty), url = it.url!!, groupId = accountId.spacerDollar(feedsGroupsMap[it.id.toString()]!!), accountId = accountId, @@ -186,6 +182,13 @@ class FeverRssService @Inject constructor( } ?: emptyList() ) + // Handle empty icon for feeds + val noIconFeeds = feedDao.queryNoIcon(accountId) + noIconFeeds.forEach { + it.icon = rssHelper.queryRssIconLink(it.url) + } + feedDao.update(*noIconFeeds.toTypedArray()) + // 3. Fetch the Fever articles (up to unlimited counts) var sinceId = account.lastArticleId?.dollarLast() ?: "" var itemsBody = feverAPI.getItemsSince(sinceId) @@ -195,7 +198,7 @@ class FeverRssService @Inject constructor( Article( id = accountId.spacerDollar(it.id!!), date = it.created_on_time?.run { Date(this * 1000) } ?: Date(), - title = Html.fromHtml(it.title ?: context.getString(R.string.empty)).toString(), + title = it.title.decodeHTML() ?: context.getString(R.string.empty), author = it.author, rawDescription = it.html ?: "", shortDescription = (Readability4JExtended("", it.html ?: "") @@ -225,7 +228,7 @@ class FeverRssService @Inject constructor( // 4. Synchronize read/unread and starred/un-starred val unreadArticleIds = feverAPI.getUnreadItems().unread_item_ids?.split(",") val starredArticleIds = feverAPI.getSavedItems().saved_item_ids?.split(",") - val articleMeta = articleDao.queryArticleMetadataAll(accountId) + val articleMeta = articleDao.queryMetadataAll(accountId) for (meta: ArticleMeta in articleMeta) { val articleId = meta.id.dollarLast() val shouldBeUnread = unreadArticleIds?.contains(articleId) diff --git a/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt b/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt index ac585157a..2ba4ae06f 100644 --- a/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt +++ b/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt @@ -1,7 +1,6 @@ package me.ash.reader.domain.service import android.content.Context -import android.text.Html import android.util.Log import androidx.work.CoroutineWorker import androidx.work.ListenableWorker @@ -28,16 +27,11 @@ import me.ash.reader.infrastructure.di.IODispatcher import me.ash.reader.infrastructure.di.MainDispatcher import me.ash.reader.infrastructure.rss.RssHelper import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI -import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI.Companion.ofCategoryIdToStreamId import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI.Companion.ofCategoryStreamIdToId -import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI.Companion.ofFeedIdToStreamId import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI.Companion.ofFeedStreamIdToId import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderAPI.Companion.ofItemStreamIdToId import me.ash.reader.infrastructure.rss.provider.greader.GoogleReaderDTO -import me.ash.reader.ui.ext.currentAccountId -import me.ash.reader.ui.ext.dollarLast -import me.ash.reader.ui.ext.showToast -import me.ash.reader.ui.ext.spacerDollar +import me.ash.reader.ui.ext.* import net.dankito.readability4j.extended.Readability4JExtended import java.util.* import javax.inject.Inject @@ -216,13 +210,13 @@ class GoogleReaderRssService @Inject constructor( val groupIds = mutableSetOf() val feedIds = mutableSetOf() val lastUpdateAt = Calendar.getInstance().apply { - if (account.updateAt != null) { - time = account.updateAt!! - add(Calendar.HOUR, -1) - } else { + // if (account.updateAt != null) { + // time = account.updateAt!! + // add(Calendar.HOUR, -1) + // } else { time = Date() add(Calendar.MONTH, -1) - } + // } }.time.time / 1000 // 1. Fetch list of feeds and folders @@ -242,12 +236,12 @@ class GoogleReaderRssService @Inject constructor( groupIds.add(groupId) // Handle feeds - feedDao.insert( - *feeds.map { + feedDao.insertOrUpdate( + feeds.map { val feedId = accountId.spacerDollar(it.id?.ofFeedStreamIdToId()!!) Feed( id = feedId, - name = it.title ?: context.getString(R.string.empty), + name = it.title.decodeHTML() ?: context.getString(R.string.empty), url = it.url!!, groupId = groupId, accountId = accountId, @@ -255,8 +249,16 @@ class GoogleReaderRssService @Inject constructor( ).also { feedIds.add(feedId) } - }.toTypedArray() + } ) + + // Handle empty icon for feeds + val noIconFeeds = feedDao.queryNoIcon(accountId) + Log.i("RLog", "sync: $noIconFeeds") + noIconFeeds.forEach { + it.icon = rssHelper.queryRssIconLink(it.url) + } + feedDao.update(*noIconFeeds.toTypedArray()) } // Remove orphaned groups and feeds @@ -273,7 +275,7 @@ class GoogleReaderRssService @Inject constructor( fetchItemsContents(unreadItems, googleReaderAPI, accountId, feedIds, unreadIds, listOf()) // 4. Fetch ids of starred items - val starredItems = googleReaderAPI.getStarredItemIds(lastUpdateAt).itemRefs + val starredItems = googleReaderAPI.getStarredItemIds().itemRefs val starredIds = starredItems?.map { it.id } fetchItemsContents(starredItems, googleReaderAPI, accountId, feedIds, unreadIds, starredIds) @@ -285,7 +287,7 @@ class GoogleReaderRssService @Inject constructor( // 7. Mark/unmark items read/starred/tagged in you app comparing // local state and ids you've got from the GoogleReader - val articlesMeta = articleDao.queryArticleMetadataAll(accountId) + val articlesMeta = articleDao.queryMetadataAll(accountId) for (meta: ArticleMeta in articlesMeta) { val articleId = meta.id.dollarLast() val shouldBeUnread = unreadIds?.contains(articleId) @@ -330,7 +332,7 @@ class GoogleReaderRssService @Inject constructor( Article( id = accountId.spacerDollar(articleId), date = it.published?.run { Date(this * 1000) } ?: Date(), - title = Html.fromHtml(it.title ?: context.getString(R.string.empty)).toString(), + title = it.title.decodeHTML() ?: context.getString(R.string.empty), author = it.author, rawDescription = it.summary?.content ?: "", shortDescription = (Readability4JExtended("", it.summary?.content ?: "") @@ -362,39 +364,42 @@ class GoogleReaderRssService @Inject constructor( isUnread: Boolean, ) { super.markAsRead(groupId, feedId, articleId, before, isUnread) + val accountId = context.currentAccountId val googleReaderAPI = getGoogleReaderAPI() - val sinceTime = before?.time - when { + val markList: List = when { groupId != null -> { - googleReaderAPI.markAllAsRead( - streamId = groupId.dollarLast().ofCategoryIdToStreamId(), - sinceTimestamp = sinceTime - ) + if (before == null) { + articleDao.queryMetadataByGroupId(accountId, groupId) + } else { + articleDao.queryMetadataByGroupId(accountId, groupId, before) + }.map { it.id.dollarLast() } } feedId != null -> { - // TODO: Nothing happened??? - googleReaderAPI.markAllAsRead( - streamId = feedId.dollarLast().ofFeedIdToStreamId(), - sinceTimestamp = sinceTime - ) + if (before == null) { + articleDao.queryMetadataByFeedId(accountId, feedId) + } else { + articleDao.queryMetadataByFeedId(accountId, feedId, before) + }.map { it.id.dollarLast() } } articleId != null -> { - googleReaderAPI.editTag( - itemIds = listOf(articleId.dollarLast()), - mark = if (!isUnread) GoogleReaderAPI.Stream.READ.tag else null, - unmark = if (isUnread) GoogleReaderAPI.Stream.READ.tag else null, - ) + listOf(articleId.dollarLast()) } else -> { - googleReaderAPI.markAllAsRead( - streamId = GoogleReaderAPI.Stream.ALL_ITEMS.tag, - sinceTimestamp = sinceTime - ) + if (before == null) { + articleDao.queryMetadataAll(accountId) + } else { + articleDao.queryMetadataAll(accountId, before) + }.map { it.id.dollarLast() } } } + if (markList.isNotEmpty()) googleReaderAPI.editTag( + itemIds = markList, + mark = if (isUnread) null else GoogleReaderAPI.Stream.READ.tag, + unmark = if (isUnread) GoogleReaderAPI.Stream.READ.tag else null, + ) } override suspend fun markAsStarred(articleId: String, isStarred: Boolean) { diff --git a/app/src/main/java/me/ash/reader/infrastructure/rss/RssHelper.kt b/app/src/main/java/me/ash/reader/infrastructure/rss/RssHelper.kt index ca0dff32a..98c51577e 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/rss/RssHelper.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/rss/RssHelper.kt @@ -1,7 +1,6 @@ package me.ash.reader.infrastructure.rss import android.content.Context -import android.text.Html import android.util.Log import com.google.gson.Gson import com.rometools.rome.feed.synd.SyndEntry @@ -17,6 +16,7 @@ import me.ash.reader.domain.model.feed.Feed import me.ash.reader.domain.repository.FeedDao import me.ash.reader.infrastructure.di.IODispatcher import me.ash.reader.ui.ext.currentAccountId +import me.ash.reader.ui.ext.decodeHTML import me.ash.reader.ui.ext.spacerDollar import net.dankito.readability4j.extended.Readability4JExtended import okhttp3.OkHttpClient @@ -112,7 +112,7 @@ class RssHelper @Inject constructor( accountId = accountId, feedId = feed.id, date = syndEntry.publishedDate ?: syndEntry.updatedDate ?: Date(), - title = Html.fromHtml(syndEntry.title ?: feed.name).toString(), + title = syndEntry.title.decodeHTML() ?: feed.name, author = syndEntry.author, rawDescription = (content ?: desc) ?: "", shortDescription = (Readability4JExtended("", desc ?: content ?: "") @@ -135,12 +135,16 @@ class RssHelper @Inject constructor( return regex.find(rawDescription)?.groupValues?.get(2)?.takeIf { !it.startsWith("data:") } } - @Throws(Exception::class) suspend fun queryRssIconLink(feedLink: String): String? { - val request = response(okHttpClient, "https://besticon-demo.herokuapp.com/allicons.json?url=${feedLink}") - val content = request.body.string() - val favicon = Gson().fromJson(content, Favicon::class.java) - return favicon?.icons?.first { it.width != null && it.width >= 20 }?.url + return try { + val request = response(okHttpClient, "https://besticon-demo.herokuapp.com/allicons.json?url=${feedLink}") + val content = request.body.string() + val favicon = Gson().fromJson(content, Favicon::class.java) + favicon?.icons?.first { it.width != null && it.width >= 20 }?.url + } catch (e: Exception) { + Log.i("RLog", "queryRssIcon is failed: ${e.message}") + null + } } suspend fun saveRssIcon(feedDao: FeedDao, feed: Feed, iconLink: String) { diff --git a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderAPI.kt b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderAPI.kt index f77cf0044..c638e35ab 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderAPI.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderAPI.kt @@ -274,6 +274,7 @@ class GoogleReaderAPI private constructor( } ) + // Not all services support it suspend fun markAllAsRead(streamId: String, sinceTimestamp: Long? = null): String = retryablePostRequest( query = "reader/api/0/mark-all-as-read", diff --git a/app/src/main/java/me/ash/reader/ui/component/FeedIcon.kt b/app/src/main/java/me/ash/reader/ui/component/FeedIcon.kt index 8bc02f54e..11c216401 100644 --- a/app/src/main/java/me/ash/reader/ui/component/FeedIcon.kt +++ b/app/src/main/java/me/ash/reader/ui/component/FeedIcon.kt @@ -27,7 +27,7 @@ fun FeedIcon( size: Dp = 20.dp, placeholderIcon: ImageVector? = null, ) { - if (iconUrl == null) { + if (iconUrl.isNullOrEmpty()) { if (placeholderIcon == null) { Box( modifier = Modifier diff --git a/app/src/main/java/me/ash/reader/ui/ext/StringExt.kt b/app/src/main/java/me/ash/reader/ui/ext/StringExt.kt index ef6bc2513..fd66ae340 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/StringExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/StringExt.kt @@ -1,5 +1,6 @@ package me.ash.reader.ui.ext +import android.text.Html import android.util.Base64 import java.math.BigInteger import java.security.MessageDigest @@ -32,3 +33,5 @@ fun String.decodeBase64(): String = String(Base64.decode(this, Base64.DEFAULT)) fun String.md5(): String = BigInteger(1, MessageDigest.getInstance("MD5").digest(toByteArray())) .toString(16).padStart(32, '0') + +fun String?.decodeHTML(): String? = this?.run { Html.fromHtml(this).toString() } 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 06f981999..3459f78bf 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 @@ -36,7 +36,7 @@ class FlowViewModel @Inject constructor( articleId: String?, conditions: MarkAsReadConditions, ) { - viewModelScope.launch { + viewModelScope.launch(ioDispatcher) { rssService.get().markAsRead( groupId = groupId, feedId = feedId, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt index 97de44667..90669f9b1 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingViewModel.kt @@ -6,25 +6,28 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.ItemSnapshotList import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import me.ash.reader.domain.model.article.Article -import me.ash.reader.domain.model.feed.Feed import me.ash.reader.domain.model.article.ArticleFlowItem import me.ash.reader.domain.model.article.ArticleWithFeed -import me.ash.reader.infrastructure.rss.RssHelper +import me.ash.reader.domain.model.feed.Feed import me.ash.reader.domain.service.RssService -import java.util.Date +import me.ash.reader.infrastructure.di.IODispatcher +import me.ash.reader.infrastructure.rss.RssHelper +import java.util.* import javax.inject.Inject @HiltViewModel class ReadingViewModel @Inject constructor( private val rssService: RssService, private val rssHelper: RssHelper, + @IODispatcher + private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { private val _readingUiState = MutableStateFlow(ReadingUiState()) @@ -40,7 +43,7 @@ class ReadingViewModel @Inject constructor( fun initData(articleId: String) { showLoading() - viewModelScope.launch { + viewModelScope.launch(ioDispatcher) { rssService.get().findArticleById(articleId)?.run { _readingUiState.update { it.copy( @@ -107,7 +110,7 @@ class ReadingViewModel @Inject constructor( fun updateReadStatus(isUnread: Boolean) { currentArticle?.run { - viewModelScope.launch { + viewModelScope.launch(ioDispatcher) { _readingUiState.update { it.copy(isUnread = isUnread) } rssService.get().markAsRead( groupId = null, @@ -125,7 +128,7 @@ class ReadingViewModel @Inject constructor( fun markAsUnread() = updateReadStatus(isUnread = true) fun updateStarredStatus(isStarred: Boolean) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(ioDispatcher) { _readingUiState.update { it.copy(isStarred = isStarred) } currentArticle?.let { rssService.get().markAsStarred( @@ -191,4 +194,4 @@ data class ReaderState( data class Error(val message: String?) : ContentState object Loading: ContentState -} \ No newline at end of file +}