Commit cae406aa authored by Christophe Henry's avatar Christophe Henry

Solves #67: Swipe right to flag article as 'read' sometime doesn't work

parent 8cb1aaee
Pipeline #3497 failed with stage
in 0 seconds
......@@ -13,6 +13,7 @@
* Fix a bug preventing to refresh when subscription lists are empty ([!62](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/62/))
* Fix [#82](https://git.feneas.org/christophehenry/freshrss-android/issues/82): home and back buttons are sometimes not displayed correctly or not displayed at all ([!71](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/71))
* Fix [#80](https://git.feneas.org/christophehenry/freshrss-android/issues/80): multiple performance bugs in the subscriptions and subscription's articles pages display ([!65](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/65))
* Fix [#67](https://git.feneas.org/christophehenry/freshrss-android/issues/67): swipe right to flag article as 'read' sometime doesn't work ([#65](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/65))
## Refactoring
......@@ -42,7 +43,7 @@
* Implements [#74](https://git.feneas.org/christophehenry/freshrss-android/issues/74): Ability to add a new feed ([!54](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/54))
## Bug fixes
* [#50](https://git.feneas.org/christophehenry/freshrss-android/issues/50): Categories are not alphabetically sorted
* [#51](https://git.feneas.org/christophehenry/freshrss-android/issues/51): Unread subscription time groups are not time-sorted
* [#55](https://git.feneas.org/christophehenry/freshrss-android/issues/55): Links click open page in the webview ([!18](https://git.feneas.org/christophehenry/freshrss-android/merge_requests/18))
......
......@@ -14,8 +14,12 @@ import fr.chenry.android.freshrss.databinding.FragmentSubscriptionArticleBinding
import fr.chenry.android.freshrss.store.database.models.Article
import fr.chenry.android.freshrss.store.database.models.Subscription
class SubscriptionArticleViewItem(val article: Article, private val subscription: Subscription) :
AbstractFlexibleItem<ArticleViewHolder>() {
class SubscriptionArticleViewItem(
private val subscription: Subscription,
val article: Article
): AbstractFlexibleItem<ArticleViewHolder>() {
init {
isDraggable = false
isSelectable = false
......@@ -30,7 +34,7 @@ class SubscriptionArticleViewItem(val article: Article, private val subscription
) = holder.bind(article)
override fun equals(other: Any?): Boolean {
if (other !is SubscriptionArticleViewItem) return false
if(other !is SubscriptionArticleViewItem) return false
return article == other.article
}
......@@ -46,7 +50,9 @@ class SubscriptionArticleViewItem(val article: Article, private val subscription
inner class ArticleViewHolder(
private val binding: FragmentSubscriptionArticleBinding,
flexibleAdapter: FlexibleAdapter<*>
) : FlexibleViewHolder(binding.root, flexibleAdapter) {
): FlexibleViewHolder(binding.root, flexibleAdapter) {
private val frontView = binding.root.findViewById<View>(R.id.fragment_subscription_article_front_view)
private val rearLeftView =
binding.root.findViewById<View>(R.id.fragment_subscription_article_rear_left_view)
......
package fr.chenry.android.freshrss.components.subscriptionarticles
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemClickListener
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.store.database.models.ReadStatus
import fr.chenry.android.freshrss.store.database.models.ReadStatus.READ
import fr.chenry.android.freshrss.store.database.models.ReadStatus.UNREAD
import fr.chenry.android.freshrss.store.database.models.Subscription
import fr.chenry.android.freshrss.store.viewmodels.SubscriptionArticlesVM
import fr.chenry.android.freshrss.store.viewmodels.SubscriptionArticlesVMF
import kotlinx.android.synthetic.main.fragment_subscription_articles.*
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.ui.alwaysUi
class SubscriptionArticlesFragment: Fragment(), Observer<List<SubscriptionArticleViewItem>> {
class SubscriptionArticlesFragment: Fragment() {
private val args: SubscriptionArticlesFragmentArgs by navArgs()
private val streamId: String by lazy {args.id}
private val subscription: Subscription by lazy {args.subscription}
private val readStatus: ReadStatus by lazy {args.readStatus}
private val model: SubscriptionArticlesVM by lazy {
ViewModelProvider(activity!!.viewModelStore, SubscriptionArticlesVMF(streamId, readStatus))
.get("stream=$streamId:readStatus=$readStatus", SubscriptionArticlesVM::class.java).apply {
liveData.observe(viewLifecycleOwner, this@SubscriptionArticlesFragment)
}
}
private val adapter by lazy {
ArticleAdapter(model.liveData.value ?: listOf())
ViewModelProvider(activity!!.viewModelStore, SubscriptionArticlesVMF(subscription, readStatus))
.get("stream=${subscription.id}:readStatus=$readStatus", SubscriptionArticlesVM::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
......@@ -44,30 +38,13 @@ class SubscriptionArticlesFragment: Fragment(), Observer<List<SubscriptionArticl
view.findViewById<RecyclerView>(R.id.fragment_subscription_article_recycler).apply {
layoutManager = LinearLayoutManager(context)
adapter = this@SubscriptionArticlesFragment.adapter
adapter = ArticleAdapter(model.liveData.value ?: listOf())
}
return view
}
override fun onChanged(articlesViewItemSubscriptions: List<SubscriptionArticleViewItem>?) {
(articlesViewItemSubscriptions ?: listOf()).let {
toggleProgressCircle(it)
adapter.updateDataSet(it, true)
}
}
fun onItemClick(position: Int): Boolean =
adapter.getItem(position)?.article?.id?.let {articleId ->
val direction = SubscriptionArticlesFragmentDirections.articlesToArticleDetail(articleId)
findNavController().navigate(direction)
true
} ?: false
fun toggleArticleReadStatus(position: Int): Promise<Unit, Exception> = adapter.getItem(position)?.let {
Store.postReadStatus(it.article, it.article.readStatus.toggle())
} ?: Promise.ofSuccess(Unit)
@SuppressLint("StringFormatInvalid")
private fun toggleProgressCircle(articles: List<*>) {
fragment_subscription_article_empty_list.text = when(readStatus) {
READ -> context?.getString(R.string.empty_subscription_list, model.subscription.title)
......@@ -98,29 +75,56 @@ class SubscriptionArticlesFragment: Fragment(), Observer<List<SubscriptionArticl
}
private inner class ArticleAdapter(items: List<SubscriptionArticleViewItem>):
FlexibleAdapter<SubscriptionArticleViewItem>(items, null, true) {
FlexibleAdapter<SubscriptionArticleViewItem>(items, null, true),
Observer<List<SubscriptionArticleViewItem>>,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemSwipeListener {
init {
model.liveData.observe(viewLifecycleOwner, this)
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
isSwipeEnabled = true
itemTouchHelperCallback.setSwipeFlags(ItemTouchHelper.RIGHT)
addListener(OnItemClickListener {_, position -> this@SubscriptionArticlesFragment.onItemClick(position)})
addListener(object: OnItemSwipeListener {
override fun onActionStateChanged(viewHolder: ViewHolder?, actionState: Int) {}
override fun onItemSwipe(position: Int, direction: Int) {
if(direction == ItemTouchHelper.RIGHT) toggleArticleReadStatus(position).alwaysUi {
if(readStatus == READ) this@ArticleAdapter.notifyItemChanged(position)
}
}
})
addListener(this as OnItemClickListener)
addListener(this as OnItemSwipeListener)
}
override fun getItemId(position: Int) =
getItem(position)?.article?.id?.hashCode()?.toLong() ?: RecyclerView.NO_ID
override fun isEmpty(): Boolean = (model.liveData.value ?: listOf()).isEmpty()
override fun onChanged(articlesViewItemSubscriptions: List<SubscriptionArticleViewItem>?) {
(articlesViewItemSubscriptions ?: listOf()).let {
toggleProgressCircle(it)
updateDataSet(it, true)
}
}
override fun onItemClick(view: View?, position: Int): Boolean = getArticleId(position)?.let {
val direction = SubscriptionArticlesFragmentDirections.articlesToArticleDetail(it)
findNavController().navigate(direction)
true
} ?: false
override fun onActionStateChanged(viewHolder: ViewHolder?, actionState: Int) {}
override fun onItemSwipe(position: Int, direction: Int) {
if(direction == ItemTouchHelper.RIGHT) toggleArticleReadStatus(position).alwaysUi {
if(readStatus == READ) this@ArticleAdapter.notifyItemChanged(position)
}
}
private fun getArticleId(position: Int) = getItem(position)?.article?.id
private fun toggleArticleReadStatus(position: Int): Promise<Unit, Exception> = getItem(position)?.let {
Store.postReadStatus(it.article, it.article.readStatus.toggle())
} ?: Promise.ofSuccess(Unit)
}
}
......@@ -42,9 +42,7 @@ class SubscriptionsFragment: Fragment(), Observer<SubscriptionViewItems> {
initRecyclerView(view)
Store.refreshingPromise.observe(
viewLifecycleOwner,
Observer {toggleProgressCircle(model.isEmpty())})
Store.refreshingPromise.observe(viewLifecycleOwner, Observer {toggleProgressCircle(model.isEmpty())})
model.subscriptions.observe(viewLifecycleOwner, this)
val contentDescription = if(category != VoidCategory) {
......@@ -88,9 +86,9 @@ class SubscriptionsFragment: Fragment(), Observer<SubscriptionViewItems> {
= toggleProgressCircle((subscriptions.isNullOrEmpty()))
fun onClick(subscription: Subscription) = when(section) {
FAVORITES -> subscription.id to READ
ALL -> subscription.id to READ
UNREAD -> subscription.id to ReadStatus.UNREAD
FAVORITES -> subscription to READ
ALL -> subscription to READ
UNREAD -> subscription to ReadStatus.UNREAD
}.let {
val direction = if(category == VoidCategory)
MainSubscriptionFragmentDirections.mainSubscriptionsToArticles(it.first, it.second) else
......
package fr.chenry.android.freshrss.store.database.models
import android.graphics.Bitmap
import android.os.Parcelable
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import androidx.databinding.library.baseAdapters.BR
......@@ -10,10 +11,12 @@ import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.api.models.SubscriptionApiItem
import fr.chenry.android.freshrss.utils.nullIfBlank
import io.reactivex.Flowable
import kotlinx.android.parcel.Parcelize
import org.joda.time.LocalDateTime
typealias Subscriptions = List<Subscription>
@Parcelize
@Entity(tableName = "subscriptions")
data class Subscription(
@PrimaryKey
......@@ -23,7 +26,7 @@ data class Subscription(
val unreadCount: Int = 0,
val subscriptionCategories: List<String> = listOf(),
val newestArticleDate: LocalDateTime = LocalDateTime(0)
) : BaseObservable(), SubscriptionDisplayable {
) : BaseObservable(), Parcelable, SubscriptionDisplayable {
@get:Bindable
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var imageBitmap: Bitmap? = null
......
......@@ -3,8 +3,7 @@ package fr.chenry.android.freshrss.store.viewmodels
import android.annotation.SuppressLint
import fr.chenry.android.freshrss.F
import fr.chenry.android.freshrss.R
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 org.joda.time.LocalDateTime
object LabelComparator: Comparator<SubscriptionCategory> {
......@@ -25,6 +24,12 @@ object DateComparator: Comparator<Subscription> {
o2.newestArticleDate.compareTo(o1.newestArticleDate)
}
object ArticleComparator: Comparator<Article> {
override fun compare(o1: Article, o2: Article): Int =
if(o1.published.isEqual(o2.published)) o1.title.compareTo(o2.title, ignoreCase = true)
else o2.published.compareTo(o1.published)
}
interface Grouper {
operator fun invoke(subscription: Subscription): String
}
......
......@@ -5,45 +5,38 @@ import fr.chenry.android.freshrss.F
import fr.chenry.android.freshrss.components.subscriptionarticles.SubscriptionArticleViewItem
import fr.chenry.android.freshrss.store.database.models.*
class SubscriptionArticlesVM(streamId: String, readStatus: ReadStatus) : ViewModel() {
val liveData: LiveData<List<SubscriptionArticleViewItem>>
var subscription: Subscription
private set
private val subscriptionLiveData: LiveData<Subscriptions>
private val source: LiveData<Articles>
init {
val comparator = Comparator<Article> { o1, o2 ->
if (o1.published.isEqual(o2.published))
o1.title.compareTo(o2.title, ignoreCase = true) else
o2.published.compareTo(o1.published)
}
val flowable = F.db.getSubcriptionsById(streamId)
subscription = flowable.blockingFirst().first()
subscriptionLiveData = flowable.toLiveData()
subscriptionLiveData.observeForever { if (it.isNotEmpty()) subscription = it.first() }
source = when (readStatus) {
ReadStatus.READ -> F.db.getArticlesByStreamId(streamId)
ReadStatus.UNREAD -> F.db.getArticleByStreamIdAndUnread(streamId)
}
sealed class SubscriptionArticlesVM(val subscription: Subscription): ViewModel() {
protected abstract val source: LiveData<Articles>
liveData = MutableLiveData<List<SubscriptionArticleViewItem>>().apply {
val liveData: LiveData<List<SubscriptionArticleViewItem>> by lazy {
MutableLiveData<List<SubscriptionArticleViewItem>>().apply {
value = listOf()
source.observeForever {
value = it.sortedWith(comparator).map { article -> SubscriptionArticleViewItem(article, subscription) }
postValue(it.sortedWith(ArticleComparator).map(::viewItemMapper))
}
}
}
private fun viewItemMapper(article: Article) = SubscriptionArticleViewItem(subscription, article)
}
class SubscriptionArticlesVMF(private val streamId: String, private val readStatus: ReadStatus) :
ViewModelProvider.NewInstanceFactory() {
class AllSubscriptionArticlesVM(subscription: Subscription): SubscriptionArticlesVM(subscription) {
override val source: LiveData<Articles> = F.db.getArticlesByStreamId(subscription.id)
}
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return SubscriptionArticlesVM(streamId, readStatus) as T
}
class UnreadSubscriptionArticlesVM(subscription: Subscription): SubscriptionArticlesVM(subscription) {
override val source: LiveData<Articles> = F.db.getArticleByStreamIdAndUnread(subscription.id)
}
class SubscriptionArticlesVMF(
private val subscription: Subscription,
private val readStatus: ReadStatus
): ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T: ViewModel?> create(modelClass: Class<T>): T = when(readStatus) {
ReadStatus.READ -> AllSubscriptionArticlesVM(subscription)
ReadStatus.UNREAD -> UnreadSubscriptionArticlesVM(subscription)
} as T
}
......@@ -38,8 +38,8 @@
android:label="@string/app_name"
tools:layout="@layout/fragment_subscription_articles">
<argument
android:name="id"
app:argType="string" />
android:name="subscription"
app:argType="fr.chenry.android.freshrss.store.database.models.Subscription" />
<argument
android:name="readStatus"
app:argType="fr.chenry.android.freshrss.store.database.models.ReadStatus" />
......
......@@ -78,7 +78,7 @@
<string name="empty_subscriptions_section_all_category">You have no subscription in %s category.\nWhy don\'t you add one?</string>
<string name="empty_subscriptions_section_unread">Yaye! You have read everything here.\nWhy don\'t go in a library to read a book?</string>
<string name="empty_subscriptions_section_unread_category">Yaye! You have read everything in %s category.\nWhy don\'t go in a library to read a book?</string>
<string name="empty_subscription_list">There is no articles on %s feed yet.\\nNew articles will probably be published soon.</string>
<string name="empty_subscription_list">There is no articles on %s feed yet.\nNew articles will probably be published soon.</string>
<string name="empty_subscription_unread_list">You have read every article on %s feed!</string>
<string name="human_time_grouping_today">Today</string>
<string name="human_time_grouping_yesterday">Yesterday</string>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment