Fever API support is coming
This commit is contained in:
parent
d105735453
commit
f99fc8698a
|
@ -32,6 +32,9 @@ class App : Application() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var localRssRepository: LocalRssRepository
|
lateinit var localRssRepository: LocalRssRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var feverRssRepository: FeverRssRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var opmlRepository: OpmlRepository
|
lateinit var opmlRepository: OpmlRepository
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.paging.PagingSource
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import me.ash.reader.DataStoreKeys
|
import me.ash.reader.DataStoreKeys
|
||||||
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
|
||||||
|
@ -42,6 +43,8 @@ abstract class AbstractRssRepository constructor(
|
||||||
val isNotSyncing: Boolean = !isSyncing
|
val isNotSyncing: Boolean = !isSyncing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract fun getSyncState(): StateFlow<SyncState>
|
||||||
|
|
||||||
abstract suspend fun updateArticleInfo(article: Article)
|
abstract suspend fun updateArticleInfo(article: Article)
|
||||||
|
|
||||||
abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
|
abstract suspend fun subscribe(feed: Feed, articles: List<Article>)
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
package me.ash.reader.data.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import me.ash.reader.DataStoreKeys
|
||||||
|
import me.ash.reader.data.account.AccountDao
|
||||||
|
import me.ash.reader.data.article.Article
|
||||||
|
import me.ash.reader.data.article.ArticleDao
|
||||||
|
import me.ash.reader.data.feed.Feed
|
||||||
|
import me.ash.reader.data.feed.FeedDao
|
||||||
|
import me.ash.reader.data.group.Group
|
||||||
|
import me.ash.reader.data.group.GroupDao
|
||||||
|
import me.ash.reader.data.source.FeverApiDataSource
|
||||||
|
import me.ash.reader.data.source.RssNetworkDataSource
|
||||||
|
import me.ash.reader.dataStore
|
||||||
|
import me.ash.reader.get
|
||||||
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FeverRssRepository @Inject constructor(
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
|
private val articleDao: ArticleDao,
|
||||||
|
private val feedDao: FeedDao,
|
||||||
|
private val groupDao: GroupDao,
|
||||||
|
private val rssHelper: RssHelper,
|
||||||
|
private val feverApiDataSource: FeverApiDataSource,
|
||||||
|
rssNetworkDataSource: RssNetworkDataSource,
|
||||||
|
accountDao: AccountDao,
|
||||||
|
workManager: WorkManager,
|
||||||
|
) : AbstractRssRepository(
|
||||||
|
context, accountDao, articleDao, groupDao,
|
||||||
|
feedDao, rssNetworkDataSource, workManager,
|
||||||
|
) {
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private val syncState = MutableStateFlow(SyncState())
|
||||||
|
|
||||||
|
override fun getSyncState() = syncState
|
||||||
|
|
||||||
|
override suspend fun updateArticleInfo(article: Article) {
|
||||||
|
articleDao.update(article)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun subscribe(feed: Feed, articles: List<Article>) {
|
||||||
|
feedDao.insert(feed)
|
||||||
|
articleDao.insertList(articles.map {
|
||||||
|
it.copy(feedId = feed.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun sync(
|
||||||
|
context: Context,
|
||||||
|
accountDao: AccountDao,
|
||||||
|
articleDao: ArticleDao,
|
||||||
|
feedDao: FeedDao,
|
||||||
|
rssNetworkDataSource: RssNetworkDataSource
|
||||||
|
) {
|
||||||
|
mutex.withLock {
|
||||||
|
val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)
|
||||||
|
?: return
|
||||||
|
|
||||||
|
syncState.update {
|
||||||
|
it.copy(
|
||||||
|
feedCount = 1,
|
||||||
|
syncedCount = 1,
|
||||||
|
currentFeedName = "Fever"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feedDao.queryAll(accountId).isNullOrEmpty()) {
|
||||||
|
// Temporary add feeds
|
||||||
|
val feverFeeds = feverApiDataSource.feeds().execute().body()!!.feeds
|
||||||
|
val feverGroupsBody = feverApiDataSource.groups().execute().body()!!
|
||||||
|
Log.i("RLog", "Fever groups: $feverGroupsBody")
|
||||||
|
feverGroupsBody.groups.forEach {
|
||||||
|
groupDao.insert(
|
||||||
|
Group(
|
||||||
|
id = it.id.toString(),
|
||||||
|
name = it.title,
|
||||||
|
accountId = accountId,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val feverFeedsGroupsMap = mutableMapOf<Int, Int>()
|
||||||
|
feverGroupsBody.feeds_groups.forEach { item ->
|
||||||
|
item.feed_ids
|
||||||
|
.split(",")
|
||||||
|
.map { it.toInt() }
|
||||||
|
.forEach { id ->
|
||||||
|
feverFeedsGroupsMap[id] = item.group_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val feeds = feverFeeds.map {
|
||||||
|
Feed(
|
||||||
|
id = it.id.toString(),
|
||||||
|
name = it.title,
|
||||||
|
url = it.url,
|
||||||
|
groupId = feverFeedsGroupsMap[it.id].toString(),
|
||||||
|
accountId = accountId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
feedDao.insertList(feeds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add articles
|
||||||
|
val articles = mutableListOf<Article>()
|
||||||
|
feverApiDataSource.itemsBySince(since = 1647444325925621L)
|
||||||
|
.execute().body()!!.items
|
||||||
|
.forEach {
|
||||||
|
articles.add(
|
||||||
|
Article(
|
||||||
|
id = it.id,
|
||||||
|
date = Date(it.created_on_time * 1000),
|
||||||
|
title = it.title,
|
||||||
|
author = it.author,
|
||||||
|
rawDescription = it.html,
|
||||||
|
shortDescription = (
|
||||||
|
Readability4JExtended("", it.html)
|
||||||
|
.parse().textContent ?: ""
|
||||||
|
).take(100).trim(),
|
||||||
|
link = it.url,
|
||||||
|
accountId = accountId,
|
||||||
|
feedId = it.feed_id.toString(),
|
||||||
|
isUnread = it.is_read == 0,
|
||||||
|
isStarred = it.is_saved == 1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
articleDao.insertList(articles)
|
||||||
|
|
||||||
|
// Complete sync
|
||||||
|
accountDao.update(accountDao.queryById(accountId)!!.apply {
|
||||||
|
updateAt = Date()
|
||||||
|
})
|
||||||
|
syncState.update {
|
||||||
|
it.copy(
|
||||||
|
feedCount = 0,
|
||||||
|
syncedCount = 0,
|
||||||
|
currentFeedName = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,8 +41,10 @@ class LocalRssRepository @Inject constructor(
|
||||||
context, accountDao, articleDao, groupDao,
|
context, accountDao, articleDao, groupDao,
|
||||||
feedDao, rssNetworkDataSource, workManager,
|
feedDao, rssNetworkDataSource, workManager,
|
||||||
) {
|
) {
|
||||||
val syncState = MutableStateFlow(SyncState())
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
private val syncState = MutableStateFlow(SyncState())
|
||||||
|
|
||||||
|
override fun getSyncState() = syncState
|
||||||
|
|
||||||
override suspend fun updateArticleInfo(article: Article) {
|
override suspend fun updateArticleInfo(article: Article) {
|
||||||
articleDao.update(article)
|
articleDao.update(article)
|
||||||
|
|
|
@ -46,10 +46,7 @@ class RssHelper @Inject constructor(
|
||||||
author = it.author,
|
author = it.author,
|
||||||
rawDescription = it.description.toString(),
|
rawDescription = it.description.toString(),
|
||||||
shortDescription = (Readability4JExtended("", it.description.toString())
|
shortDescription = (Readability4JExtended("", it.description.toString())
|
||||||
.parse().textContent ?: "").trim().run {
|
.parse().textContent ?: "").take(100).trim(),
|
||||||
if (this.length > 100) this.substring(0, 100)
|
|
||||||
else this
|
|
||||||
},
|
|
||||||
link = it.link ?: "",
|
link = it.link ?: "",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -112,10 +109,7 @@ class RssHelper @Inject constructor(
|
||||||
author = it.author,
|
author = it.author,
|
||||||
rawDescription = it.description.toString(),
|
rawDescription = it.description.toString(),
|
||||||
shortDescription = (Readability4JExtended("", it.description.toString())
|
shortDescription = (Readability4JExtended("", it.description.toString())
|
||||||
.parse().textContent ?: "").trim().run {
|
.parse().textContent ?: "").take(100).trim(),
|
||||||
if (this.length > 100) this.substring(0, 100)
|
|
||||||
else this
|
|
||||||
},
|
|
||||||
link = it.link ?: "",
|
link = it.link ?: "",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,13 +12,13 @@ class RssRepository @Inject constructor(
|
||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val localRssRepository: LocalRssRepository,
|
private val localRssRepository: LocalRssRepository,
|
||||||
// private val feverRssRepository: FeverRssRepository,
|
private val feverRssRepository: FeverRssRepository,
|
||||||
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
// private val googleReaderRssRepository: GoogleReaderRssRepository,
|
||||||
) {
|
) {
|
||||||
fun get() = when (getAccountType()) {
|
fun get() = when (getAccountType()) {
|
||||||
// Account.Type.LOCAL -> localRssRepository
|
|
||||||
Account.Type.LOCAL -> localRssRepository
|
Account.Type.LOCAL -> localRssRepository
|
||||||
// Account.Type.FEVER -> feverRssRepository
|
// Account.Type.LOCAL -> 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: ${getAccountType()}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,15 +10,38 @@ import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.http.Multipart
|
import retrofit2.http.Multipart
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Part
|
import retrofit2.http.Part
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface FeverApiDataSource {
|
interface FeverApiDataSource {
|
||||||
@Multipart
|
@Multipart
|
||||||
@POST("fever.php?api&groups")
|
@POST("fever.php/?api=&feeds=")
|
||||||
|
fun feeds(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Feed>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST("fever.php/?api=&groups=")
|
||||||
fun groups(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Groups>
|
fun groups(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Groups>
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@POST("fever.php?api&feeds")
|
@POST("fever.php/?api=&items=")
|
||||||
fun feeds(@Part("api_key") apiKey: RequestBody?="1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.Feed>
|
fun itemsBySince(
|
||||||
|
@Query("since_id") since: Long,
|
||||||
|
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()
|
||||||
|
): Call<FeverApiDto.Items>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST("fever.php/?api=&unread_item_ids=")
|
||||||
|
fun itemsByUnread(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.ItemsByUnread>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST("fever.php/?api=&saved_item_ids=")
|
||||||
|
fun itemsByStarred(@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()): Call<FeverApiDto.ItemsByStarred>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST("fever.php/?api=&items=")
|
||||||
|
fun itemsByIds(
|
||||||
|
@Query("with_ids") ids: String,
|
||||||
|
@Part("api_key") apiKey: RequestBody? = "1352b707f828a6f502db3768fa8d7151".toRequestBody()
|
||||||
|
): Call<FeverApiDto.Items>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: FeverApiDataSource? = null
|
private var instance: FeverApiDataSource? = null
|
||||||
|
|
|
@ -1,13 +1,78 @@
|
||||||
package me.ash.reader.data.source
|
package me.ash.reader.data.source
|
||||||
|
|
||||||
object FeverApiDto {
|
object FeverApiDto {
|
||||||
// &groups
|
|
||||||
data class Groups(
|
/**
|
||||||
val apiVersion: Int,
|
* @link fever.php/?api=&feeds=
|
||||||
|
* @sample
|
||||||
|
* {
|
||||||
|
* "api_version": 3,
|
||||||
|
* "auth": 1,
|
||||||
|
* "last_refreshed_on_time": 1647530101,
|
||||||
|
* "feeds": [
|
||||||
|
* {
|
||||||
|
* "id": 2,
|
||||||
|
* "favicon_id": 2,
|
||||||
|
* "title": "Ash's Knowledge Base",
|
||||||
|
* "url": "https://www.ashinch.com/feed",
|
||||||
|
* "site_url": "http://ashinch.com/",
|
||||||
|
* "is_spark": 0,
|
||||||
|
* "last_updated_on_time": 1647530101
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "feeds_groups": [
|
||||||
|
* {
|
||||||
|
* "group_id": 2,
|
||||||
|
* "feed_ids": "2,3,4"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
data class Feed(
|
||||||
|
val api_version: Int,
|
||||||
val auth: Int,
|
val auth: Int,
|
||||||
val lastRefreshedOnTime: Long,
|
val last_refreshed_on_time: Long,
|
||||||
|
val feeds: List<FeedItem>,
|
||||||
|
val feeds_groups: List<FeedsGroupsItem>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FeedItem(
|
||||||
|
val id: Int,
|
||||||
|
val favicon_id: Int,
|
||||||
|
val title: String,
|
||||||
|
val url: String,
|
||||||
|
val site_url: String,
|
||||||
|
val is_spark: Int,
|
||||||
|
val last_refreshed_on_time: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @link fever.php/?api=&groups=
|
||||||
|
* @sample
|
||||||
|
* {
|
||||||
|
* "api_version": 3,
|
||||||
|
* "auth": 1,
|
||||||
|
* "last_refreshed_on_time": 1647534602,
|
||||||
|
* "groups": [
|
||||||
|
* {
|
||||||
|
* "id": 1,
|
||||||
|
* "title": "未分类"
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "feeds_groups": [
|
||||||
|
* {
|
||||||
|
* "group_id": 2,
|
||||||
|
* "feed_ids": "2,3,4"
|
||||||
|
* },
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
data class Groups(
|
||||||
|
val api_version: Int,
|
||||||
|
val auth: Int,
|
||||||
|
val last_refreshed_on_time: Long,
|
||||||
val groups: List<GroupItem>,
|
val groups: List<GroupItem>,
|
||||||
val feedsGroups: List<FeedsGroupsItem>,
|
val feeds_groups: List<FeedsGroupsItem>,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GroupItem(
|
data class GroupItem(
|
||||||
|
@ -16,39 +81,85 @@ object FeverApiDto {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class FeedsGroupsItem(
|
data class FeedsGroupsItem(
|
||||||
val groupId: Int,
|
val group_id: Int,
|
||||||
val feedsIds: String,
|
val feed_ids: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
// &feeds
|
/**
|
||||||
data class Feed(
|
* @link fever.php/?api=&items=&with_ids={ids}
|
||||||
val apiVersion: Int,
|
* @link fever.php/?api=&items=&since_id={since}
|
||||||
|
* @sample
|
||||||
|
* {
|
||||||
|
* "api_version": 3,
|
||||||
|
* "auth": 1,
|
||||||
|
* "last_refreshed_on_time": 1647534602,
|
||||||
|
* "total_items": 853,
|
||||||
|
* "items": [
|
||||||
|
* {
|
||||||
|
* "id": "1647445533955157",
|
||||||
|
* "feed_id": 37,
|
||||||
|
* "title": "智能音箱自己把自己黑了:随机购物拨号,自主开灯关门,平均成功率达88%",
|
||||||
|
* "author": "博雯",
|
||||||
|
* "html": "<blockquote>\n<p data-track=\"48\">博雯 发自 凹非寺</p>\n<p d...",
|
||||||
|
* "url": "https://www.qbitai.com/2022/03/33402.html",
|
||||||
|
* "is_saved": 0,
|
||||||
|
* "is_read": 0,
|
||||||
|
* "created_on_time": 1647442680
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* {
|
||||||
|
*/
|
||||||
|
data class Items(
|
||||||
|
val api_version: Int,
|
||||||
val auth: Int,
|
val auth: Int,
|
||||||
val lastRefreshedOnTime: Long,
|
val last_refreshed_on_time: Long,
|
||||||
val feeds: List<FeedItem>,
|
val total_items: Int,
|
||||||
val feedsGroups: List<FeedsGroupsItem>,
|
val items: List<Item>,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class FeedItem(
|
data class Item(
|
||||||
val id: Int,
|
val id: String,
|
||||||
val favicon_id: Int,
|
val feed_id: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
val author: String,
|
||||||
|
val html: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val siteUrl: String,
|
val is_saved: Int,
|
||||||
val isSpark: Int,
|
val is_read: Int,
|
||||||
val lastRefreshedOnTime: Long,
|
val created_on_time: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
// &favicons
|
/**
|
||||||
data class Favicons(
|
* @link fever.php/?api=&unread_item_ids=
|
||||||
val apiVersion: Int,
|
* @sample
|
||||||
|
* {
|
||||||
|
* "api_version": 3,
|
||||||
|
* "auth": 1,
|
||||||
|
* "last_refreshed_on_time": 1647530135,
|
||||||
|
* "unread_item_ids": "1646660589277217,1646660589277218"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
data class ItemsByUnread(
|
||||||
|
val api_version: Int,
|
||||||
val auth: Int,
|
val auth: Int,
|
||||||
val lastRefreshedOnTime: Long,
|
val last_refreshed_on_time: Long,
|
||||||
val favicons: List<FaviconItem>,
|
val unread_item_ids: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class FaviconItem(
|
/**
|
||||||
val id: Int,
|
* @link fever.php/?api=&saved_item_ids=
|
||||||
val data: String,
|
* @sample
|
||||||
|
* {
|
||||||
|
* "api_version": 3,
|
||||||
|
* "auth": 1,
|
||||||
|
* "last_refreshed_on_time": 1647534602,
|
||||||
|
* "saved_item_ids": "1647441026698935,1646660589277218"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
data class ItemsByStarred(
|
||||||
|
val api_version: Int,
|
||||||
|
val auth: Int,
|
||||||
|
val last_refreshed_on_time: Long,
|
||||||
|
val saved_item_ids: String,
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -31,7 +31,7 @@ class HomeViewModel @Inject constructor(
|
||||||
private val _filterState = MutableStateFlow(FilterState())
|
private val _filterState = MutableStateFlow(FilterState())
|
||||||
val filterState = _filterState.asStateFlow()
|
val filterState = _filterState.asStateFlow()
|
||||||
|
|
||||||
val syncState = rssRepository.get().syncState
|
val syncState = rssRepository.get().getSyncState()
|
||||||
|
|
||||||
fun dispatch(action: HomeViewAction) {
|
fun dispatch(action: HomeViewAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user