Refactor repository coroutine
This commit is contained in:
parent
004005d8be
commit
e1e43019f5
|
@ -5,10 +5,11 @@ import androidx.hilt.work.HiltWorkerFactory
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.ash.reader.data.module.ApplicationScope
|
import me.ash.reader.data.module.ApplicationScope
|
||||||
|
import me.ash.reader.data.module.DispatcherDefault
|
||||||
import me.ash.reader.data.repository.*
|
import me.ash.reader.data.repository.*
|
||||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
import me.ash.reader.data.source.OpmlLocalDataSource
|
||||||
import me.ash.reader.data.source.ReaderDatabase
|
import me.ash.reader.data.source.ReaderDatabase
|
||||||
|
@ -57,9 +58,13 @@ class App : Application(), Configuration.Provider {
|
||||||
@ApplicationScope
|
@ApplicationScope
|
||||||
lateinit var applicationScope: CoroutineScope
|
lateinit var applicationScope: CoroutineScope
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@DispatcherDefault
|
||||||
|
lateinit var dispatcherDefault: CoroutineDispatcher
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
applicationScope.launch(Dispatchers.IO) {
|
applicationScope.launch(dispatcherDefault) {
|
||||||
accountInit()
|
accountInit()
|
||||||
workerInit()
|
workerInit()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ import java.io.IOException
|
||||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
|
||||||
val Context.currentAccountId: Int
|
val Context.currentAccountId: Int
|
||||||
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!!
|
||||||
|
val Context.currentAccountType: Int
|
||||||
|
get() = this.dataStore.get(DataStoreKeys.CurrentAccountType)!!
|
||||||
|
|
||||||
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
|
suspend fun <T> DataStore<Preferences>.put(dataStoreKeys: DataStoreKeys<T>, value: T) {
|
||||||
this.edit {
|
this.edit {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import androidx.paging.PagingSource
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import me.ash.reader.currentAccountId
|
import me.ash.reader.currentAccountId
|
||||||
|
@ -32,6 +32,7 @@ abstract class AbstractRssRepository constructor(
|
||||||
private val feedDao: FeedDao,
|
private val feedDao: FeedDao,
|
||||||
private val rssNetworkDataSource: RssNetworkDataSource,
|
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||||
private val workManager: WorkManager,
|
private val workManager: WorkManager,
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
data class SyncState(
|
data class SyncState(
|
||||||
val feedCount: Int = 0,
|
val feedCount: Int = 0,
|
||||||
|
@ -59,11 +60,11 @@ abstract class AbstractRssRepository constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullGroups(): Flow<MutableList<Group>> {
|
fun pullGroups(): Flow<MutableList<Group>> {
|
||||||
return groupDao.queryAllGroup(context.currentAccountId).flowOn(Dispatchers.IO)
|
return groupDao.queryAllGroup(context.currentAccountId).flowOn(dispatcherIO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
|
fun pullFeeds(): Flow<MutableList<GroupWithFeed>> {
|
||||||
return groupDao.queryAllGroupWithFeedAsFlow(context.currentAccountId).flowOn(Dispatchers.IO)
|
return groupDao.queryAllGroupWithFeedAsFlow(context.currentAccountId).flowOn(dispatcherIO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pullArticles(
|
fun pullArticles(
|
||||||
|
@ -72,7 +73,6 @@ abstract class AbstractRssRepository constructor(
|
||||||
isStarred: Boolean = false,
|
isStarred: Boolean = false,
|
||||||
isUnread: Boolean = false,
|
isUnread: Boolean = false,
|
||||||
): PagingSource<Int, ArticleWithFeed> {
|
): PagingSource<Int, ArticleWithFeed> {
|
||||||
Log.i("RLog", "thread:pullArticles ${Thread.currentThread().name}")
|
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
Log.i(
|
Log.i(
|
||||||
"RLog",
|
"RLog",
|
||||||
|
@ -107,7 +107,6 @@ abstract class AbstractRssRepository constructor(
|
||||||
isStarred: Boolean = false,
|
isStarred: Boolean = false,
|
||||||
isUnread: Boolean = false,
|
isUnread: Boolean = false,
|
||||||
): Flow<List<ImportantCount>> {
|
): Flow<List<ImportantCount>> {
|
||||||
Log.i("RLog", "thread:pullImportant ${Thread.currentThread().name}")
|
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
Log.i(
|
Log.i(
|
||||||
"RLog",
|
"RLog",
|
||||||
|
@ -119,7 +118,7 @@ abstract class AbstractRssRepository constructor(
|
||||||
isUnread -> articleDao
|
isUnread -> articleDao
|
||||||
.queryImportantCountWhenIsUnread(accountId, isUnread)
|
.queryImportantCountWhenIsUnread(accountId, isUnread)
|
||||||
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
else -> articleDao.queryImportantCountWhenIsAll(accountId)
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(dispatcherIO)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findFeedById(id: String): Feed? {
|
suspend fun findFeedById(id: String): Feed? {
|
||||||
|
@ -130,7 +129,7 @@ abstract class AbstractRssRepository constructor(
|
||||||
return articleDao.queryById(id)
|
return articleDao.queryById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isExist(url: String): Boolean {
|
suspend fun isFeedExist(url: String): Boolean {
|
||||||
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
|
return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,11 @@ import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import me.ash.reader.*
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.account.AccountDao
|
import me.ash.reader.data.account.AccountDao
|
||||||
import me.ash.reader.data.article.Article
|
import me.ash.reader.data.article.Article
|
||||||
import me.ash.reader.data.article.ArticleDao
|
import me.ash.reader.data.article.ArticleDao
|
||||||
|
@ -13,11 +16,16 @@ import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.feed.FeedDao
|
import me.ash.reader.data.feed.FeedDao
|
||||||
import me.ash.reader.data.group.Group
|
import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.data.group.GroupDao
|
import me.ash.reader.data.group.GroupDao
|
||||||
|
import me.ash.reader.data.module.ApplicationScope
|
||||||
|
import me.ash.reader.data.module.DispatcherDefault
|
||||||
|
import me.ash.reader.data.module.DispatcherIO
|
||||||
import me.ash.reader.data.source.FeverApiDataSource
|
import me.ash.reader.data.source.FeverApiDataSource
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
|
import me.ash.reader.spacerDollar
|
||||||
import net.dankito.readability4j.extended.Readability4JExtended
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.set
|
||||||
|
|
||||||
class FeverRssRepository @Inject constructor(
|
class FeverRssRepository @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
|
@ -29,10 +37,17 @@ class FeverRssRepository @Inject constructor(
|
||||||
private val feverApiDataSource: FeverApiDataSource,
|
private val feverApiDataSource: FeverApiDataSource,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
rssNetworkDataSource: RssNetworkDataSource,
|
rssNetworkDataSource: RssNetworkDataSource,
|
||||||
|
@ApplicationScope
|
||||||
|
private val applicationScope: CoroutineScope,
|
||||||
|
@DispatcherDefault
|
||||||
|
private val dispatcherDefault: CoroutineDispatcher,
|
||||||
|
@DispatcherIO
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
workManager: WorkManager,
|
workManager: WorkManager,
|
||||||
) : AbstractRssRepository(
|
) : AbstractRssRepository(
|
||||||
context, accountDao, articleDao, groupDao,
|
context, accountDao, articleDao, groupDao,
|
||||||
feedDao, rssNetworkDataSource, workManager,
|
feedDao, rssNetworkDataSource, workManager,
|
||||||
|
dispatcherIO
|
||||||
) {
|
) {
|
||||||
override suspend fun updateArticleInfo(article: Article) {
|
override suspend fun updateArticleInfo(article: Article) {
|
||||||
articleDao.update(article)
|
articleDao.update(article)
|
||||||
|
@ -58,6 +73,7 @@ class FeverRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sync() {
|
override suspend fun sync() {
|
||||||
|
applicationScope.launch(dispatcherDefault) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
|
|
||||||
|
@ -143,4 +159,5 @@ class FeverRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -11,11 +11,7 @@ import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import me.ash.reader.MainActivity
|
import me.ash.reader.MainActivity
|
||||||
import me.ash.reader.R
|
import me.ash.reader.R
|
||||||
import me.ash.reader.currentAccountId
|
import me.ash.reader.currentAccountId
|
||||||
|
@ -26,6 +22,9 @@ import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.feed.FeedDao
|
import me.ash.reader.data.feed.FeedDao
|
||||||
import me.ash.reader.data.group.Group
|
import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.data.group.GroupDao
|
import me.ash.reader.data.group.GroupDao
|
||||||
|
import me.ash.reader.data.module.ApplicationScope
|
||||||
|
import me.ash.reader.data.module.DispatcherDefault
|
||||||
|
import me.ash.reader.data.module.DispatcherIO
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import me.ash.reader.ui.page.common.ExtraName
|
import me.ash.reader.ui.page.common.ExtraName
|
||||||
import me.ash.reader.ui.page.common.NotificationGroupName
|
import me.ash.reader.ui.page.common.NotificationGroupName
|
||||||
|
@ -41,10 +40,17 @@ class LocalRssRepository @Inject constructor(
|
||||||
private val rssNetworkDataSource: RssNetworkDataSource,
|
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
private val groupDao: GroupDao,
|
private val groupDao: GroupDao,
|
||||||
|
@ApplicationScope
|
||||||
|
private val applicationScope: CoroutineScope,
|
||||||
|
@DispatcherDefault
|
||||||
|
private val dispatcherDefault: CoroutineDispatcher,
|
||||||
|
@DispatcherIO
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
workManager: WorkManager,
|
workManager: WorkManager,
|
||||||
) : AbstractRssRepository(
|
) : AbstractRssRepository(
|
||||||
context, accountDao, articleDao, groupDao,
|
context, accountDao, articleDao, groupDao,
|
||||||
feedDao, rssNetworkDataSource, workManager,
|
feedDao, rssNetworkDataSource, workManager,
|
||||||
|
dispatcherIO
|
||||||
) {
|
) {
|
||||||
private val notificationManager: NotificationManager =
|
private val notificationManager: NotificationManager =
|
||||||
(getSystemService(
|
(getSystemService(
|
||||||
|
@ -84,8 +90,7 @@ class LocalRssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sync() {
|
override suspend fun sync() {
|
||||||
mutex.withLock {
|
applicationScope.launch(dispatcherDefault) {
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val preTime = System.currentTimeMillis()
|
val preTime = System.currentTimeMillis()
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
val articles = mutableListOf<Article>()
|
val articles = mutableListOf<Article>()
|
||||||
|
@ -116,8 +121,7 @@ class LocalRssRepository @Inject constructor(
|
||||||
currentFeedName = ""
|
currentFeedName = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}.join()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ArticleNotify(
|
data class ArticleNotify(
|
||||||
|
@ -127,10 +131,20 @@ class LocalRssRepository @Inject constructor(
|
||||||
|
|
||||||
private suspend fun syncFeed(feed: Feed): ArticleNotify {
|
private suspend fun syncFeed(feed: Feed): ArticleNotify {
|
||||||
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
|
val latest = articleDao.queryLatestByFeedId(context.currentAccountId, feed.id)
|
||||||
val articles = rssHelper.queryRssXml(feed, latest?.link).also {
|
var articles: List<Article>? = null
|
||||||
if (feed.icon == null && it.isNotEmpty()) {
|
try {
|
||||||
rssHelper.queryRssIcon(feedDao, feed, it.first().link)
|
articles = rssHelper.queryRssXml(feed, latest?.link)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("RLog", "queryRssXml[${feed.name}]: ${e.message}")
|
||||||
|
return ArticleNotify(listOf(), false)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
if (feed.icon == null && !articles.isNullOrEmpty()) {
|
||||||
|
rssHelper.queryRssIcon(feedDao, feed, articles.first().link)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("RLog", "queryRssIcon[${feed.name}]: ${e.message}")
|
||||||
|
return ArticleNotify(listOf(), false)
|
||||||
}
|
}
|
||||||
updateSyncState {
|
updateSyncState {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
package me.ash.reader.data.repository
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import be.ceau.opml.OpmlWriter
|
import be.ceau.opml.OpmlWriter
|
||||||
import be.ceau.opml.entity.Body
|
import be.ceau.opml.entity.Body
|
||||||
import be.ceau.opml.entity.Head
|
import be.ceau.opml.entity.Head
|
||||||
import be.ceau.opml.entity.Opml
|
import be.ceau.opml.entity.Opml
|
||||||
import be.ceau.opml.entity.Outline
|
import be.ceau.opml.entity.Outline
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import me.ash.reader.R
|
||||||
import me.ash.reader.currentAccountId
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.account.AccountDao
|
import me.ash.reader.data.account.AccountDao
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.feed.FeedDao
|
import me.ash.reader.data.feed.FeedDao
|
||||||
import me.ash.reader.data.group.GroupDao
|
import me.ash.reader.data.group.GroupDao
|
||||||
import me.ash.reader.data.source.OpmlLocalDataSource
|
import me.ash.reader.data.source.OpmlLocalDataSource
|
||||||
|
import me.ash.reader.spacerDollar
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -25,11 +26,12 @@ class OpmlRepository @Inject constructor(
|
||||||
private val feedDao: FeedDao,
|
private val feedDao: FeedDao,
|
||||||
private val accountDao: AccountDao,
|
private val accountDao: AccountDao,
|
||||||
private val rssRepository: RssRepository,
|
private val rssRepository: RssRepository,
|
||||||
private val opmlLocalDataSource: OpmlLocalDataSource
|
private val opmlLocalDataSource: OpmlLocalDataSource,
|
||||||
|
private val stringsRepository: StringsRepository,
|
||||||
) {
|
) {
|
||||||
|
@Throws(Exception::class)
|
||||||
suspend fun saveToDatabase(inputStream: InputStream) {
|
suspend fun saveToDatabase(inputStream: InputStream) {
|
||||||
try {
|
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
|
||||||
val defaultGroup = groupDao.queryById(opmlLocalDataSource.getDefaultGroupId())!!
|
|
||||||
val groupWithFeedList =
|
val groupWithFeedList =
|
||||||
opmlLocalDataSource.parseFileInputStream(inputStream, defaultGroup)
|
opmlLocalDataSource.parseFileInputStream(inputStream, defaultGroup)
|
||||||
groupWithFeedList.forEach { groupWithFeed ->
|
groupWithFeedList.forEach { groupWithFeed ->
|
||||||
|
@ -39,21 +41,18 @@ class OpmlRepository @Inject constructor(
|
||||||
val repeatList = mutableListOf<Feed>()
|
val repeatList = mutableListOf<Feed>()
|
||||||
groupWithFeed.feeds.forEach {
|
groupWithFeed.feeds.forEach {
|
||||||
it.groupId = groupWithFeed.group.id
|
it.groupId = groupWithFeed.group.id
|
||||||
if (rssRepository.get().isExist(it.url)) {
|
if (rssRepository.get().isFeedExist(it.url)) {
|
||||||
repeatList.add(it)
|
repeatList.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList())
|
feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("saveToDatabase", "${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveToString(): String? =
|
@Throws(Exception::class)
|
||||||
try {
|
suspend fun saveToString(): String {
|
||||||
val defaultGroup = groupDao.queryById(opmlLocalDataSource.getDefaultGroupId())!!
|
val defaultGroup = groupDao.queryById(getDefaultGroupId())!!
|
||||||
OpmlWriter().write(
|
return OpmlWriter().write(
|
||||||
Opml(
|
Opml(
|
||||||
"2.0",
|
"2.0",
|
||||||
Head(
|
Head(
|
||||||
|
@ -85,9 +84,12 @@ class OpmlRepository @Inject constructor(
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)!!
|
||||||
} catch (e: Exception) {
|
}
|
||||||
Log.e("saveToString", "${e.message}")
|
|
||||||
null
|
private fun getDefaultGroupId(): String {
|
||||||
|
val readYouString = stringsRepository.getString(R.string.read_you)
|
||||||
|
val defaultString = stringsRepository.getString(R.string.defaults)
|
||||||
|
return context.currentAccountId.spacerDollar(readYouString + defaultString)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,17 +4,20 @@ import android.content.Context
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import me.ash.reader.currentAccountId
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.article.Article
|
import me.ash.reader.data.article.Article
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.feed.FeedDao
|
import me.ash.reader.data.feed.FeedDao
|
||||||
import me.ash.reader.data.feed.FeedWithArticle
|
import me.ash.reader.data.feed.FeedWithArticle
|
||||||
|
import me.ash.reader.data.module.DispatcherIO
|
||||||
import me.ash.reader.data.source.RssNetworkDataSource
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
import me.ash.reader.spacerDollar
|
import me.ash.reader.spacerDollar
|
||||||
import net.dankito.readability4j.Readability4J
|
import net.dankito.readability4j.Readability4J
|
||||||
import net.dankito.readability4j.extended.Readability4JExtended
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
import okhttp3.*
|
import okhttp3.OkHttpClient
|
||||||
import java.io.IOException
|
import okhttp3.Request
|
||||||
import java.text.ParsePosition
|
import java.text.ParsePosition
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -24,9 +27,12 @@ class RssHelper @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val rssNetworkDataSource: RssNetworkDataSource,
|
private val rssNetworkDataSource: RssNetworkDataSource,
|
||||||
|
@DispatcherIO
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
suspend fun searchFeed(feedLink: String): FeedWithArticle {
|
||||||
|
return withContext(dispatcherIO) {
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
val parseRss = rssNetworkDataSource.parseRss(feedLink)
|
val parseRss = rssNetworkDataSource.parseRss(feedLink)
|
||||||
val feed = Feed(
|
val feed = Feed(
|
||||||
|
@ -36,7 +42,8 @@ class RssHelper @Inject constructor(
|
||||||
groupId = "",
|
groupId = "",
|
||||||
accountId = accountId,
|
accountId = accountId,
|
||||||
)
|
)
|
||||||
return FeedWithArticle(feed, queryRssXml(feed))
|
FeedWithArticle(feed, queryRssXml(feed))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseDescriptionContent(link: String, content: String): String {
|
fun parseDescriptionContent(link: String, content: String): String {
|
||||||
|
@ -46,42 +53,39 @@ class RssHelper @Inject constructor(
|
||||||
return element.toString()
|
return element.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseFullContent(link: String, title: String, callback: (String) -> Unit) {
|
@Throws(Exception::class)
|
||||||
OkHttpClient()
|
suspend fun parseFullContent(link: String, title: String): String {
|
||||||
|
return withContext(dispatcherIO) {
|
||||||
|
val response = OkHttpClient()
|
||||||
.newCall(Request.Builder().url(link).build())
|
.newCall(Request.Builder().url(link).build())
|
||||||
.enqueue(object : Callback {
|
.execute()
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
val content = response.body!!.string()
|
||||||
callback(e.message.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
|
||||||
val content = response.body?.string()
|
|
||||||
val readability4J: Readability4J =
|
val readability4J: Readability4J =
|
||||||
Readability4JExtended(link, content ?: "")
|
Readability4JExtended(link, content)
|
||||||
val articleContent = readability4J.parse().articleContent
|
val articleContent = readability4J.parse().articleContent
|
||||||
if (articleContent == null) {
|
if (articleContent == null) {
|
||||||
callback("")
|
""
|
||||||
} else {
|
} else {
|
||||||
val h1Element = articleContent.selectFirst("h1")
|
val h1Element = articleContent.selectFirst("h1")
|
||||||
if (h1Element != null && h1Element.hasText() && h1Element.text() == title) {
|
if (h1Element != null && h1Element.hasText() && h1Element.text() == title) {
|
||||||
h1Element.remove()
|
h1Element.remove()
|
||||||
}
|
}
|
||||||
callback(articleContent.toString())
|
articleContent.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
suspend fun queryRssXml(
|
suspend fun queryRssXml(
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
latestLink: String? = null,
|
latestLink: String? = null,
|
||||||
): List<Article> {
|
): List<Article> {
|
||||||
|
return withContext(dispatcherIO) {
|
||||||
val a = mutableListOf<Article>()
|
val a = mutableListOf<Article>()
|
||||||
try {
|
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
val parseRss = rssNetworkDataSource.parseRss(feed.url)
|
val parseRss = rssNetworkDataSource.parseRss(feed.url)
|
||||||
parseRss.items.forEach {
|
parseRss.items.forEach {
|
||||||
if (latestLink != null && latestLink == it.link) return a
|
if (latestLink != null && latestLink == it.link) return@withContext a
|
||||||
Log.i("RLog", "request rss ${feed.name}: ${it.title}")
|
Log.i("RLog", "request rss ${feed.name}: ${it.title}")
|
||||||
a.add(
|
a.add(
|
||||||
Article(
|
Article(
|
||||||
|
@ -104,27 +108,23 @@ class RssHelper @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return a
|
a
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("RLog", "error ${feed.name}: ${e.message}")
|
|
||||||
return a
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
suspend fun queryRssIcon(
|
suspend fun queryRssIcon(
|
||||||
feedDao: FeedDao,
|
feedDao: FeedDao,
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
articleLink: String?,
|
articleLink: String,
|
||||||
) {
|
) {
|
||||||
try {
|
withContext(dispatcherIO) {
|
||||||
if (articleLink == null) return
|
|
||||||
val execute = OkHttpClient()
|
val execute = OkHttpClient()
|
||||||
.newCall(Request.Builder().url(articleLink).build())
|
.newCall(Request.Builder().url(articleLink).build())
|
||||||
.execute()
|
.execute()
|
||||||
val content = execute.body?.string()
|
val content = execute.body!!.string()
|
||||||
val regex =
|
val regex =
|
||||||
Regex("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
|
Regex("""<link(.+?)rel="shortcut icon"(.+?)href="(.+?)"""")
|
||||||
if (content != null) {
|
|
||||||
var iconLink = regex
|
var iconLink = regex
|
||||||
.find(content)
|
.find(content)
|
||||||
?.groups?.get(3)
|
?.groups?.get(3)
|
||||||
|
@ -144,24 +144,22 @@ class RssHelper @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
// saveRssIcon(feedDao, feed, "")
|
// saveRssIcon(feedDao, feed, "")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// saveRssIcon(feedDao, feed, "")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("RLog", "queryRssIcon: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveRssIcon(feedDao: FeedDao, feed: Feed, iconLink: String) {
|
@Throws(Exception::class)
|
||||||
val execute = OkHttpClient()
|
suspend fun saveRssIcon(feedDao: FeedDao, feed: Feed, iconLink: String) {
|
||||||
|
withContext(dispatcherIO) {
|
||||||
|
val response = OkHttpClient()
|
||||||
.newCall(Request.Builder().url(iconLink).build())
|
.newCall(Request.Builder().url(iconLink).build())
|
||||||
.execute()
|
.execute()
|
||||||
feedDao.update(
|
feedDao.update(
|
||||||
feed.apply {
|
feed.apply {
|
||||||
icon = execute.body?.bytes()
|
icon = response.body!!.bytes()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseDate(
|
private fun parseDate(
|
||||||
inputDate: String, patterns: Array<String?> = arrayOf(
|
inputDate: String, patterns: Array<String?> = arrayOf(
|
||||||
|
|
|
@ -2,10 +2,8 @@ package me.ash.reader.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import me.ash.reader.DataStoreKeys
|
import me.ash.reader.currentAccountType
|
||||||
import me.ash.reader.data.account.Account
|
import me.ash.reader.data.account.Account
|
||||||
import me.ash.reader.dataStore
|
|
||||||
import me.ash.reader.get
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RssRepository @Inject constructor(
|
class RssRepository @Inject constructor(
|
||||||
|
@ -15,13 +13,11 @@ class RssRepository @Inject constructor(
|
||||||
private val feverRssRepository: FeverRssRepository,
|
private val feverRssRepository: FeverRssRepository,
|
||||||
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
||||||
) {
|
) {
|
||||||
fun get() = when (getAccountType()) {
|
fun get() = when (context.currentAccountType) {
|
||||||
Account.Type.LOCAL -> localRssRepository
|
Account.Type.LOCAL -> localRssRepository
|
||||||
// Account.Type.LOCAL -> feverRssRepository
|
// Account.Type.LOCAL -> feverRssRepository
|
||||||
Account.Type.FEVER -> feverRssRepository
|
Account.Type.FEVER -> feverRssRepository
|
||||||
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
|
// Account.Type.GOOGLE_READER -> googleReaderRssRepository
|
||||||
else -> throw IllegalStateException("Unknown account type: ${getAccountType()}")
|
else -> throw IllegalStateException("Unknown account type: ${context.currentAccountType}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAccountType(): Int = context.dataStore.get(DataStoreKeys.CurrentAccountType)!!
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,13 @@ package me.ash.reader.data.source
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import be.ceau.opml.OpmlParser
|
import be.ceau.opml.OpmlParser
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import me.ash.reader.*
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import me.ash.reader.currentAccountId
|
||||||
import me.ash.reader.data.feed.Feed
|
import me.ash.reader.data.feed.Feed
|
||||||
import me.ash.reader.data.group.Group
|
import me.ash.reader.data.group.Group
|
||||||
import me.ash.reader.data.group.GroupWithFeed
|
import me.ash.reader.data.group.GroupWithFeed
|
||||||
import me.ash.reader.data.repository.StringsRepository
|
import me.ash.reader.data.module.DispatcherIO
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -15,18 +17,15 @@ import javax.inject.Inject
|
||||||
class OpmlLocalDataSource @Inject constructor(
|
class OpmlLocalDataSource @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val stringsRepository: StringsRepository,
|
@DispatcherIO
|
||||||
|
private val dispatcherIO: CoroutineDispatcher,
|
||||||
) {
|
) {
|
||||||
fun getDefaultGroupId(): String {
|
@Throws(Exception::class)
|
||||||
val readYouString = stringsRepository.getString(R.string.read_you)
|
suspend fun parseFileInputStream(
|
||||||
val defaultString = stringsRepository.getString(R.string.defaults)
|
inputStream: InputStream,
|
||||||
return context.dataStore
|
defaultGroup: Group
|
||||||
.get(DataStoreKeys.CurrentAccountId)!!
|
): List<GroupWithFeed> {
|
||||||
.spacerDollar(readYouString + defaultString)
|
return withContext(dispatcherIO) {
|
||||||
}
|
|
||||||
|
|
||||||
// @Throws(XmlPullParserException::class, IOException::class)
|
|
||||||
fun parseFileInputStream(inputStream: InputStream, defaultGroup: Group): List<GroupWithFeed> {
|
|
||||||
val accountId = context.currentAccountId
|
val accountId = context.currentAccountId
|
||||||
val opml = OpmlParser().parse(inputStream)
|
val opml = OpmlParser().parse(inputStream)
|
||||||
val groupWithFeedList = mutableListOf<GroupWithFeed>().also {
|
val groupWithFeedList = mutableListOf<GroupWithFeed>().also {
|
||||||
|
@ -87,7 +86,8 @@ class OpmlLocalDataSource @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return groupWithFeedList
|
groupWithFeedList
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<GroupWithFeed>.addGroup(group: Group) {
|
private fun MutableList<GroupWithFeed>.addGroup(group: Group) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package me.ash.reader.ui.page.home.drawer.feed
|
package me.ash.reader.ui.page.home.drawer.feed
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
@ -8,10 +9,10 @@ import androidx.compose.material.icons.rounded.DeleteOutline
|
||||||
import androidx.compose.material.icons.rounded.RssFeed
|
import androidx.compose.material.icons.rounded.RssFeed
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
@ -31,7 +32,6 @@ fun FeedOptionDrawer(
|
||||||
viewModel: FeedOptionViewModel = hiltViewModel(),
|
viewModel: FeedOptionViewModel = hiltViewModel(),
|
||||||
content: @Composable () -> Unit = {},
|
content: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val viewState = viewModel.viewState.collectAsStateValue()
|
val viewState = viewModel.viewState.collectAsStateValue()
|
||||||
val feed = viewState.feed
|
val feed = viewState.feed
|
||||||
|
|
||||||
|
|
|
@ -49,14 +49,22 @@ class FeedsViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun importFromInputStream(inputStream: InputStream) {
|
private fun importFromInputStream(inputStream: InputStream) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
opmlRepository.saveToDatabase(inputStream)
|
opmlRepository.saveToDatabase(inputStream)
|
||||||
rssRepository.get().doSync()
|
rssRepository.get().doSync()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("FeedsViewModel", "importFromInputStream: ", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportAsOpml(callback: (String) -> Unit = {}) {
|
private fun exportAsOpml(callback: (String) -> Unit = {}) {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
opmlRepository.saveToString()?.let { callback(it) }
|
try {
|
||||||
|
callback(opmlRepository.saveToString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("FeedsViewModel", "exportAsOpml: ", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +82,6 @@ class FeedsViewModel @Inject constructor(
|
||||||
rssRepository.get().pullFeeds(),
|
rssRepository.get().pullFeeds(),
|
||||||
rssRepository.get().pullImportant(isStarred, isUnread),
|
rssRepository.get().pullImportant(isStarred, isUnread),
|
||||||
) { groupWithFeedList, importantList ->
|
) { groupWithFeedList, importantList ->
|
||||||
Log.i("RLog", "thread:combine ${Thread.currentThread().name}")
|
|
||||||
val groupImportantMap = mutableMapOf<String, Int>()
|
val groupImportantMap = mutableMapOf<String, Int>()
|
||||||
val feedImportantMap = mutableMapOf<String, Int>()
|
val feedImportantMap = mutableMapOf<String, Int>()
|
||||||
importantList.groupBy { it.groupId }.forEach { (i, list) ->
|
importantList.groupBy { it.groupId }.forEach { (i, list) ->
|
||||||
|
@ -109,8 +116,6 @@ class FeedsViewModel @Inject constructor(
|
||||||
}.onStart {
|
}.onStart {
|
||||||
|
|
||||||
}.onEach { groupWithFeedList ->
|
}.onEach { groupWithFeedList ->
|
||||||
Log.i("RLog", "thread:onEach ${Thread.currentThread().name}")
|
|
||||||
|
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
filter = when {
|
filter = when {
|
||||||
|
|
|
@ -148,7 +148,7 @@ class SubscribeViewModel @Inject constructor(
|
||||||
lockLinkInput = true,
|
lockLinkInput = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (rssRepository.get().isExist(_viewState.value.linkContent)) {
|
if (rssRepository.get().isFeedExist(_viewState.value.linkContent)) {
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
title = stringsRepository.getString(R.string.subscribe),
|
title = stringsRepository.getString(R.string.subscribe),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package me.ash.reader.ui.page.home.flow
|
package me.ash.reader.ui.page.home.flow
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
@ -55,7 +54,6 @@ class FlowViewModel @Inject constructor(
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
pagingData = Pager(PagingConfig(pageSize = 10)) {
|
pagingData = Pager(PagingConfig(pageSize = 10)) {
|
||||||
Log.i("RLog", "thread:Pager ${Thread.currentThread().name}")
|
|
||||||
rssRepository.get().pullArticles(
|
rssRepository.get().pullArticles(
|
||||||
groupId = filterState.group?.id,
|
groupId = filterState.group?.id,
|
||||||
feedId = filterState.feed?.id,
|
feedId = filterState.feed?.id,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package me.ash.reader.ui.page.home.read
|
package me.ash.reader.ui.page.home.read
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
@ -55,12 +56,23 @@ class ReadViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun renderFullContent() {
|
private fun renderFullContent() {
|
||||||
changeLoading(true)
|
changeLoading(true)
|
||||||
rssHelper.parseFullContent(
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
_viewState.update {
|
||||||
|
it.copy(
|
||||||
|
content = rssHelper.parseFullContent(
|
||||||
_viewState.value.articleWithFeed?.article?.link ?: "",
|
_viewState.value.articleWithFeed?.article?.link ?: "",
|
||||||
_viewState.value.articleWithFeed?.article?.title ?: ""
|
_viewState.value.articleWithFeed?.article?.title ?: ""
|
||||||
) { content ->
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.i("RLog", "renderFullContent: ${e.message}")
|
||||||
_viewState.update {
|
_viewState.update {
|
||||||
it.copy(content = content)
|
it.copy(
|
||||||
|
content = e.message
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user