Commit 4939d695 authored by Christophe Henry's avatar Christophe Henry

Separate feed to tags relation into own table

- Fixes #83: old categories are not removed
- Implements #46: compute unread articles count for subscription category
parent 39fb6651
Pipeline #3822 passed with stage
in 0 seconds
......@@ -4,12 +4,14 @@
## Features
* Implement [#46](https://git.feneas.org/christophehenry/freshrss-android/issues/46): compute unread articles count for subscription category ([!89](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/89))
* Better handle images embedded in a link by showing the link seperatly ([!63](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/63))
* Add a notification to report crashes ([!67](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/67))
* Implement [#52](https://git.feneas.org/christophehenry/freshrss-android/issues/52): support favorites ([!88](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/88))
## Bug fixes
* Fix [#83](https://git.feneas.org/christophehenry/freshrss-android/issues/83): old categories are not removed after refresh ([!89](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/89))
* Fix [#89](https://git.feneas.org/christophehenry/freshrss-android/issues/89): feed title is not correctly removed from article title ([!87](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/87))
* Fix [#84](https://git.feneas.org/christophehenry/freshrss-android/issues/84): article view crashing on Android 5.0 and 5.1 ([!74](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/74))
* Fix a bug preventing to refresh when subscription lists are empty ([!62](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/62/))
......
......@@ -120,6 +120,7 @@ dependencies {
def retrofit_version = "2.7.2"
def okhttp_version = "4.4.1"
def work_version = "2.3.3"
def mockk_version = "1.9.3"
// Linter
ktlint "com.github.shyiko:ktlint:0.31.0"
......@@ -177,7 +178,7 @@ dependencies {
// WorkManager
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation 'com.google.guava:guava:28.2-jre'
implementation "com.google.guava:guava:28.2-jre"
// Utils
......@@ -218,6 +219,7 @@ dependencies {
* The Software shall be used for Good, not Evil.
*/
testImplementation "org.json:json:20190722"
testImplementation "io.mockk:mockk:$mockk_version"
androidTestImplementation "com.github.javafaker:javafaker:1.0.2"
androidTestImplementation "androidx.test:rules:$android_test"
androidTestImplementation "androidx.test:runner:$android_test"
......@@ -225,6 +227,9 @@ dependencies {
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version"
androidTestImplementation "androidx.work:work-testing:$work_version"
// TODO: Workaround; See https://github.com/mockk/mockk/issues/281#issuecomment-479238315
androidTestImplementation("io.mockk:mockk:$mockk_version") { exclude module: "objenesis" }
androidTestImplementation "org.objenesis:objenesis:2.6"
// Debug
......
......@@ -56,7 +56,6 @@ class FreshRSSDabatabaseTest: FreshRSSDabatabaseBaseTest() {
TestUtils.harryPotter().character(),
"${oldSubscription.iconUrl}/icon",
oldSubscription.unreadCount + 20,
oldSubscription.subscriptionCategories + TestUtils.harryPotter().character().split("\\s+".toRegex()),
oldSubscription.newestArticleDate.plusDays(3)
)
subscriptionsMap[newSubscription.id] = newSubscription
......@@ -65,8 +64,7 @@ class FreshRSSDabatabaseTest: FreshRSSDabatabaseBaseTest() {
val expected = oldSubscription.copy(
title = newSubscription.title,
iconUrl = newSubscription.iconUrl,
subscriptionCategories = newSubscription.subscriptionCategories
iconUrl = newSubscription.iconUrl
)
val actual = db.getSubcriptionsById(oldSubscription.id).blockingFirst().firstOrNull()
......
......@@ -10,11 +10,10 @@ object SubscriptionsTestUtils {
title: String = TestUtils.lebowski().character(),
iconUrl: String = TestUtils.internet().url(),
unreadCount: Int = TestUtils.number().numberBetween(0, 100),
subscriptionCategories: List<String> = listOf(),
newestArticleDate: LocalDateTime = LocalDateTime(
TestUtils.jodaDate().past(15,TimeUnit.DAYS).millis
)
) = Subscription(id, title, iconUrl, unreadCount, subscriptionCategories, newestArticleDate)
) = Subscription(id, title, iconUrl, unreadCount, newestArticleDate)
fun createSubscriptions(number: Int) = (1..number).map {createSubscription()}
}
......@@ -86,6 +86,8 @@ open class FreshRSSApplication: Application(), SharedPreferences.OnSharedPrefere
RefreshWorker.manualWorkRequest
).await()
}
enqueuePeriodicRequest()
}
open fun cancelOngoinrefresh(): Unit = runBlocking {
......
......@@ -61,6 +61,7 @@ class RefreshWorker(appContext: Context, workerParams: WorkerParameters): Corout
val result = runCatching {
setForeground(foregroundInfo)
Store.getTags()
Store.getSubscriptions()
Store.getUnreadCount()
Store.getStreamContents(ALL_ITEMS_ID)
......
......@@ -12,7 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionSection.*
import fr.chenry.android.freshrss.components.subscriptions.adapters.SubscriptionViewItems
import fr.chenry.android.freshrss.components.subscriptions.adapters.FeedLikeViewItems
import fr.chenry.android.freshrss.components.subscriptions.adapters.SubscriptionsFlexibleAdapter
import fr.chenry.android.freshrss.store.database.models.*
import fr.chenry.android.freshrss.store.viewmodels.*
......@@ -20,7 +20,7 @@ import fr.chenry.android.freshrss.utils.EmotionnalImageSubtext
import fr.chenry.android.freshrss.utils.requireF
import kotlinx.android.synthetic.main.fragment_subscriptions.*
class SubscriptionsFragment: Fragment(), Observer<SubscriptionViewItems> {
class SubscriptionsFragment: Fragment(), Observer<FeedLikeViewItems> {
private val args: SubscriptionsFragmentArgs by navArgs()
private val section by lazy {args.section}
private val category by lazy {args.category}
......@@ -76,17 +76,21 @@ class SubscriptionsFragment: Fragment(), Observer<SubscriptionViewItems> {
return view
}
override fun onChanged(subscriptions: SubscriptionViewItems?) =
override fun onChanged(subscriptions: FeedLikeViewItems?) =
toggleProgressCircle((subscriptions.isNullOrEmpty()))
fun onClick(subscription: Subscription) = when(category) {
VoidCategory -> MainSubscriptionFragmentDirections.mainSubscriptionsToArticles(subscription, section)
else -> SubscriptionsFragmentDirections.articlesCategoryToArticles(subscription, section)
}.let(findNavController()::navigate)
fun onClick(virtualFeed: FeedLikeDisplayable): Boolean {
if(virtualFeed is Subscription) when(category) {
VoidCategory -> MainSubscriptionFragmentDirections.mainSubscriptionsToArticles(virtualFeed, section)
else -> SubscriptionsFragmentDirections.articlesCategoryToArticles(virtualFeed, section)
}.let(findNavController()::navigate)
fun onClick(subscriptionCategory: SubscriptionCategory) =
MainSubscriptionFragmentDirections.mainSubscriptionsToArticlesCategory(subscriptionCategory, section)
.let(findNavController()::navigate)
if(virtualFeed is FeedTag)
MainSubscriptionFragmentDirections.mainSubscriptionsToArticlesCategory(virtualFeed, section)
.let(findNavController()::navigate)
return true
}
private fun initRecyclerView(view: View) {
val adapter = SubscriptionsFlexibleAdapter(this)
......
......@@ -9,15 +9,20 @@ import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import fr.chenry.android.freshrss.BR
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.components.subscriptions.adapters.AbstractSubscriptionViewItem.SubscriptionViewHolder
import fr.chenry.android.freshrss.components.subscriptions.adapters.FeedLikeViewItem.SubscriptionViewHolder
import fr.chenry.android.freshrss.databinding.FragmentSubscriptionBinding
import fr.chenry.android.freshrss.store.database.models.Subscription
import fr.chenry.android.freshrss.store.database.models.SubscriptionCategory
import fr.chenry.android.freshrss.store.database.models.*
import fr.chenry.android.freshrss.store.viewmodels.Grouper
import fr.chenry.android.freshrss.utils.unit
typealias FeedLikeSectionnableItem<T> = AbstractSectionableItem<T, SubscriptionViewHeaderItem>
typealias FeedLikeViewItems = List<FeedLikeViewItem>
class FeedLikeViewItem(
val virtualFeed: FeedLikeDisplayable,
grouper: Grouper
): FeedLikeSectionnableItem<SubscriptionViewHolder>(getHeader(virtualFeed, grouper)) {
typealias SubscriptionViewItems = List<AbstractSubscriptionViewItem>
sealed class AbstractSubscriptionViewItem(header: SubscriptionViewHeaderItem) :
AbstractSectionableItem<SubscriptionViewHolder, SubscriptionViewHeaderItem>(header) {
abstract val virtualSubscription: Subscription
init {
isDraggable = false
......@@ -35,38 +40,31 @@ sealed class AbstractSubscriptionViewItem(header: SubscriptionViewHeaderItem) :
holder: SubscriptionViewHolder?,
position: Int,
payloads: MutableList<Any>?
) = holder?.bind(virtualSubscription).let { Unit }
) = holder?.bind(virtualFeed).unit()
override fun getLayoutRes() = R.layout.fragment_subscription
override fun hashCode(): Int = virtualFeed.hashCode()
override fun equals(other: Any?): Boolean =
if(other !is FeedLikeViewItem) false else virtualFeed == other.virtualFeed
inner class SubscriptionViewHolder(
private val binding: FragmentSubscriptionBinding,
adapter: FlexibleAdapter<IFlexible<ViewHolder>>?
) : FlexibleViewHolder(binding.root, adapter) {
fun bind(subscription: Subscription) {
binding.setVariable(BR.subscription, subscription)
): FlexibleViewHolder(binding.root, adapter) {
fun bind(virtualFeed: FeedLikeDisplayable) {
binding.setVariable(BR.feed, virtualFeed)
binding.executePendingBindings()
}
}
}
class SubscriptionViewItem(val subscription: Subscription, header: String) :
AbstractSubscriptionViewItem(SubscriptionViewHeaderItem.of(header)) {
override val virtualSubscription: Subscription get() = subscription
override fun equals(other: Any?): Boolean =
if (other !is SubscriptionViewItem) false else subscription == other.subscription
override fun hashCode(): Int = subscription.hashCode()
}
class SubscriptionCategoryViewItem(val subscriptionCategory: SubscriptionCategory) :
AbstractSubscriptionViewItem(SubscriptionCategoryViewHeaderItem) {
override val virtualSubscription: Subscription =
Subscription(subscriptionCategory.id, subscriptionCategory.label, "", unreadCount = -1)
override fun equals(other: Any?): Boolean =
if (other !is SubscriptionCategoryViewItem) false else subscriptionCategory == other.subscriptionCategory
override fun hashCode(): Int = subscriptionCategory.hashCode()
companion object {
fun getHeader(virtualFeed: FeedLikeDisplayable, grouper: Grouper) = when(virtualFeed) {
is FeedTag -> SubscriptionCategoryViewHeaderItem
else -> SubscriptionViewHeaderItem.of(grouper(virtualFeed as Subscription))
}
}
}
......@@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
import fr.chenry.android.freshrss.components.subscriptions.SubscriptionsFragment
class SubscriptionsFlexibleAdapter(private val fragment: SubscriptionsFragment) :
FlexibleAdapter<AbstractSubscriptionViewItem>(listOf(), null, true),
FlexibleAdapter<FeedLikeViewItem>(listOf(), null, true),
OnItemClickListener {
init {
......@@ -22,27 +22,21 @@ class SubscriptionsFlexibleAdapter(private val fragment: SubscriptionsFragment)
override fun onCreateBubbleText(position: Int): String? {
return when (val item = getGenericItem(position)) {
is AbstractSubscriptionViewItem -> item.header.title
is FeedLikeViewItem -> item.header.title
is SubscriptionViewHeaderItem -> item.title
else -> null
}
}
override fun onItemClick(view: View?, position: Int): Boolean = getGenericItem(position)?.let {
when (it) {
is SubscriptionViewItem -> fragment.onClick(it.subscription)
is SubscriptionCategoryViewItem -> fragment.onClick(it.subscriptionCategory)
}
true
override fun onItemClick(view: View, position: Int): Boolean = getGenericItem(position)?.let {
if(it is FeedLikeViewItem) fragment.onClick(it.virtualFeed)
else false
} ?: false
private fun getGenericItem(position: Int): IFlexible<*>? = (this as FlexibleAdapter<*>).getItem(position)
override fun getItemId(position: Int): Long = getGenericItem(position)?.let {
when (it) {
is SubscriptionViewItem -> it.subscription.id.hashCode().toLong()
is SubscriptionCategoryViewItem -> it.subscriptionCategory.id.hashCode().toLong()
else -> super.getItemId(position)
}
if(it is FeedLikeViewItem) it.virtualFeed.id.hashCode().toLong()
else super.getItemId(position)
} ?: super.getItemId(position)
private fun getGenericItem(position: Int): IFlexible<*>? = (this as FlexibleAdapter<*>).getItem(position)
}
......@@ -67,13 +67,25 @@ object Store {
}
}
suspend fun getTags() {
executeSafeApiCall {getTags()}.onSuccess {result ->
F.db.syncAllSubscriptionCategories(result.map(FeedTag.Companion::fromFeedTagItem))
}
}
suspend fun getSubscriptions() {
val result = executeApiCallInIO {getSubscriptions()}
withContext(Dispatchers.IO) {
val subscriptions = result.map {it.id to Subscription.fromSubscriptionApiItem(it)}.toMap()
executeSafeApiCall {getSubscriptions()}.onSuccess {result ->
withContext(Dispatchers.IO) {
val feedInsertAsync = async {
val subscriptions = result.map {it.id to Subscription.fromSubscriptionApiItem(it)}.toMap()
F.db.syncSubscriptions(subscriptions)
}
val syncDeferred = async {
F.db.syncSubscriptions(subscriptions)
val feedTagRelationInsertAsync = async {
result.forEach {F.db.syncFeedToTagRelationForFeed(it.id, it.categories)}
}
listOf(feedInsertAsync, feedTagRelationInsertAsync).awaitAll()
// TODO: Extract
val asyncs = F.db.getAllSubcriptionsWithImageToUpdate().map {
......@@ -81,48 +93,45 @@ object Store {
}
asyncs.awaitAll()
}
val categoriesDeferred = async {
val categories = result.flatMap {it.categories}.map(SubscriptionCategory.Companion::fromApiItem)
F.db.insertAllSubscriptionCategories(categories)
}
listOf(syncDeferred, categoriesDeferred).awaitAll()
}
}
suspend fun getUnreadCount() {
val unreadCounts = executeApiCallInIO {getUnreadCount()}
withContext(Dispatchers.IO) {
val asyncs = unreadCounts.unreadcounts.map {async {F.db.updateSubscriptionCount(it.id, it.count)}}
val updateAsync = async {F.db.updateTotalUnreadCount(account.value!!.id, unreadCounts.max)}
(listOf(updateAsync) + asyncs).awaitAll()
executeSafeApiCall {getUnreadCount()}.onSuccess {result ->
withContext(Dispatchers.IO) {
val asyncs = result.unreadcounts.map {async {F.db.updateSubscriptionCount(it.id, it.count)}}
val updateAsync = async {F.db.updateTotalUnreadCount(account.value!!.id, result.max)}
(listOf(updateAsync) + asyncs).awaitAll()
}
}
}
suspend fun getStreamContents(id: String) {
val contents = executeApiCallInIO {getStreamContents(id, lastFetchTimestamp.toString())}
withContext(Dispatchers.IO) {
val articles = contents.items.map(Article.Companion::fromContentItem)
val articlesInsertAsync = async {F.db.insertArticles(articles)}
val insertAsync = contents.items.map {
async {F.db.updateSubscriptionNewestArticleDate(it.origin.streamId, it.crawled)}
executeSafeApiCall {getStreamContents(id, lastFetchTimestamp.toString())}.onSuccess {result ->
withContext(Dispatchers.IO) {
val articles = result.items.map(Article.Companion::fromContentItem)
val articlesInsertAsync = async {F.db.insertArticles(articles)}
val insertAsync = result.items.map {
async {F.db.updateSubscriptionNewestArticleDate(it.origin.streamId, it.crawled)}
}
(insertAsync + articlesInsertAsync).awaitAll()
}
(insertAsync + articlesInsertAsync).awaitAll()
}
}
suspend fun getUnreadItemIds() {
val unreadItems = executeApiCallInIO {getItemIds(mapOf("xt" to READ_ITEMS_ID))}
withContext(Dispatchers.IO) {
F.db.syncAllArticlesReadStatus(unreadItems)
executeSafeApiCall {getItemIds(mapOf("xt" to READ_ITEMS_ID))}.onSuccess {
withContext(Dispatchers.IO) {
F.db.syncAllArticlesReadStatus(it)
}
}
}
suspend fun getFavoriteItemIds() {
val unreadItems = executeApiCallInIO {getItemIds(mapOf("s" to FAVORITE_ITEMS_ID))}
withContext(Dispatchers.IO) {
F.db.syncAllArticlesFavoriteStatus(unreadItems)
executeSafeApiCall {getItemIds(mapOf("s" to FAVORITE_ITEMS_ID))}.onSuccess {
withContext(Dispatchers.IO) {
F.db.syncAllArticlesFavoriteStatus(it)
}
}
}
......@@ -138,7 +147,7 @@ object Store {
val result = resultAsync.await()
result.onSuccess {
F.db.insertArticle(article.copy(readStatus = readStatus))
F.db.updateArticleReadStatuses(listOf(ArticleReadStatusUpdate(article.id, readStatus)))
when(readStatus) {
ReadStatus.READ -> {
F.db.decrementSubscriptionCount(article.streamId)
......@@ -165,7 +174,7 @@ object Store {
val result = resultAsync.await()
result.onSuccess {
F.db.insertArticle(article.copy(favorite = favoriteStatus))
F.db.updateArticleFavoriteStatuses(listOf(ArticleFavoriteStatusUpdate(article.id, favoriteStatus)))
}
result
......@@ -182,16 +191,13 @@ object Store {
}
}
private suspend inline fun <T: Any> executeApiCallInIO(crossinline block: API.() -> Call<T>): T =
withContext(Dispatchers.IO) {
private suspend inline fun <T: Any> executeSafeApiCall(crossinline block: API.() -> Call<T>): Result<T> =
runCatching {
if(!F.context.isConnectedToNetwork()) throw NoNetworkException()
if(service != null) {
val result = block(service!!).execute()
if(result.isSuccessful) return@withContext result.body()!!
val result = withContext(Dispatchers.IO) {block(service!!).execute()}
if(result.isSuccessful) return@runCatching result.body()!!
else throw ServerException(result.raw())
} else throw NotConnectedException()
}
private suspend inline fun <T: Any> executeSafeApiCall(crossinline block: API.() -> Call<T>): Result<T> =
runCatching {executeApiCallInIO(block)}
}
......@@ -52,7 +52,7 @@ interface API {
@AuthorisationRequired
@UseDeserializer(TagsConverter::class)
@GET("reader/api/0/tag/list")
fun getTags(): Call<List<String>>
fun getTags(): Call<List<FeedTagItem>>
@FormUrlEncoded
@TokenRequired
......
......@@ -25,6 +25,7 @@ data class ContentItem(
val timestamp: LocalDateTime,
@JsonDeserialize(using = PublishedDateDeserializer::class)
val published: LocalDateTime,
@JsonDeserialize(using = HtmlEntitiesDeserializer::class)
val title: String,
val alternate: List<Href>,
val categories: List<String>,
......
package fr.chenry.android.freshrss.store.api.models
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
data class FeedTagItem(
val id: String,
val type: String?,
@JsonProperty("unread_count")
val unreadCount: Int = 0
) {
@JsonIgnore
val label: String = id.replace("user/-/label/", "")
}
package fr.chenry.android.freshrss.store.api.models
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.deser.std.StringDeserializer
import fr.chenry.android.freshrss.utils.unescape
import fr.chenry.android.freshrss.store.api.utils.FeedCategoriesDeserializer
import fr.chenry.android.freshrss.store.api.utils.HtmlEntitiesDeserializer
data class SubscriptionApiItem(
val id: String,
@JsonDeserialize(using = HtmlEntitiesDeserializer::class)
val title: String,
val categories: List<SubscriptionCategoryApiItem> = listOf(),
@JsonDeserialize(using = FeedCategoriesDeserializer::class)
val categories: List<String> = listOf(),
val url: String,
val htmlUrl: String,
val iconUrl: String
)
data class SubscriptionCategoryApiItem(
val id: String,
val label: String
)
class HtmlEntitiesDeserializer : StringDeserializer() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?) = super.deserialize(p, ctxt).unescape()
}
......@@ -46,9 +46,7 @@ internal class APIInterceptor(private val account: Account): Interceptor {
}
private object FLogger: HttpLoggingInterceptor.Logger {
override fun log(message: String) = runCatching { // Prevent fail during tests
Log.d(API::class.qualifiedName!!, message)
}.unit()
override fun log(message: String) = Log.d(API::class.qualifiedName!!, message).unit()
}
internal val loggingInterceptor = HttpLoggingInterceptor(FLogger).apply {
......
package fr.chenry.android.freshrss.store.api.utils
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.TreeNode
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.deser.std.StringDeserializer
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.datatype.joda.JodaModule
import com.fasterxml.jackson.datatype.joda.deser.LocalDateTimeDeserializer
......@@ -9,7 +11,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import fr.chenry.android.freshrss.store.api.models.*
import fr.chenry.android.freshrss.store.database.models.Article
import fr.chenry.android.freshrss.utils.Try
import fr.chenry.android.freshrss.utils.*
import okhttp3.ResponseBody
import org.joda.time.DateTimeZone
import org.joda.time.LocalDateTime
......@@ -17,12 +19,16 @@ import org.json.JSONArray
import org.json.JSONObject
import retrofit2.Converter
class HtmlEntitiesDeserializer: StringDeserializer() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?) = super.deserialize(p, ctxt).unescape()
}
class ContentItemsDeserializer: JsonDeserializer<ContentItems>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ContentItems = Try {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ContentItems = runCatching {
JACKSON_OBJECT_MAPPER.readTree<ArrayNode>(p).mapNotNull {
Try {JACKSON_OBJECT_MAPPER.convertValue(it, ContentItem::class.java)}.getOrNull()
runCatching {JACKSON_OBJECT_MAPPER.convertValue(it, ContentItem::class.java)}.onFailure(this::e).getOrNull()
}
}.getOrDefault(listOf())
}.onFailure(this::e).getOrDefault(listOf())
}
class MicroSecTimestampDeserializer: LocalDateTimeDeserializer() {
......@@ -46,18 +52,26 @@ class PublishedDateDeserializer: LocalDateTimeDeserializer() {
}
}
object TagsConverter: Converter<ResponseBody, List<String>> {
override fun convert(value: ResponseBody): List<String> = Try {
class FeedCategoriesDeserializer: JsonDeserializer<List<String>>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): List<String> = runCatching {
val jsonArray = JSONArray(p.codec.readTree<TreeNode>(p).toString())
(0 until jsonArray.length()).mapNotNull {
runCatching {jsonArray.optJSONObject(it).optString("id")}.onFailure(this::e).getOrNull()
}
}.onFailure(this::e).getOrDefault(listOf())
}
object TagsConverter: Converter<ResponseBody, List<FeedTagItem>> {
override fun convert(value: ResponseBody): List<FeedTagItem> = kotlin.runCatching {
value.use {
val jsonArray = JSONObject(value.string()).optJSONArray("tags")
(0 until jsonArray.length()).mapNotNull {
val tag = jsonArray.optJSONObject(it)
if(tag.optString("type") == "folder") tag.optString("id")
else null
val result = JACKSON_OBJECT_MAPPER.readValue<FeedTagItem>(jsonArray.optJSONObject(it).toString())
if(result.type == null) null else result
}
}
}.getOrDefault(listOf())
}.onFailure(this::e).getOrDefault(listOf())
}
object SubscriptionApiItemsConverter: Converter<ResponseBody, List<SubscriptionApiItem>> {
......@@ -66,7 +80,7 @@ object SubscriptionApiItemsConverter: Converter<ResponseBody, List<SubscriptionA
val jsonArray = JSONObject(value.string()).optJSONArray("subscriptions")
JACKSON_OBJECT_MAPPER.readValue<List<SubscriptionApiItem>>(jsonArray.toString())
}
}.getOrDefault(listOf())
}.onFailure(this::e).getOrDefault(listOf())
}