Commit 7ef529c4 authored by Christophe Henry's avatar Christophe Henry

Merge branch 'grouping-subscription-by' into 'develop'

Add sectionning of subscriptions

See merge request !5
parents 02423ab0 6dcd628a
......@@ -125,6 +125,7 @@ dependencies {
implementation "org.apache.commons:commons-text:1.4"
implementation "joda-time:joda-time:2.10.1"
implementation "com.squareup.picasso:picasso:2.71828"
implementation "io.github.luizgrp.sectionedrecyclerviewadapter:sectionedrecyclerviewadapter:2.0.0"
// Tests
testImplementation "junit:junit:4.12"
......
package fr.chenry.android.freshrss.components.subscriptions
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.View
import android.widget.TextView
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.RecyclerView
import fr.chenry.android.freshrss.BR
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.databinding.FragmentSubscriptionBinding
import fr.chenry.android.freshrss.store.database.models.Subscription
class RecyclerViewAdapter(private val fragment: SubscriptionsFragment):
RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>()
{
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding: FragmentSubscriptionBinding
= DataBindingUtil.inflate(layoutInflater, R.layout.fragment_subscription, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val subscription = fragment.subscriptions[position]
holder.bind(subscription)
holder.binding.root.setOnClickListener {fragment.onClick(subscription)}
import fr.chenry.android.freshrss.store.database.models.Subscriptions
import io.github.luizgrp.sectionedrecyclerviewadapter.*
class RecyclerViewAdapter(private val fragment: SubscriptionsFragment): SectionedRecyclerViewAdapter() {
init {
fragment.model.sectionnedLiveData.observe(fragment, Observer {
this.removeAllSections()
it.entries.forEach {self -> this.addSection(self.key, RecyclerViewAdapterSection(self.toPair()))}
this.notifyDataSetChanged()
})
}
override fun getItemCount(): Int = fragment.subscriptions.size
inner class ViewHolder(val binding: FragmentSubscriptionBinding):
RecyclerView.ViewHolder(binding.root) {
inner class ViewHolder(val binding: FragmentSubscriptionBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(subscription: Subscription) {
binding.setVariable(BR.subscription, subscription)
binding.executePendingBindings()
}
}
inner class RecyclerViewAdapterSection(private val entry: Pair<String, Subscriptions>):
StatelessSection(getSectionParameters()) {
override fun onBindItemViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val subscription = entry.second[position]
(holder as ViewHolder).apply {
bind(subscription)
binding.root.setOnClickListener {fragment.onClick(subscription)}
}
}
override fun onBindHeaderViewHolder(holder: RecyclerView.ViewHolder) {
holder.itemView.findViewById<TextView>(R.id.subscription_section_title).text = entry.first
}
override fun getItemViewHolder(view: View) = ViewHolder(DataBindingUtil.bind(view)!!)
override fun getContentItemsTotal() = entry.second.size
}
companion object {
fun getSectionParameters(): SectionParameters =
SectionParameters.builder()
.headerResourceId(R.layout.fragment_subscription_header)
.itemResourceId(R.layout.fragment_subscription)
.build()
}
}
......@@ -23,7 +23,7 @@ class SubscriptionsFragment: Fragment(), Observer<Subscriptions> {
arguments?.getParcelable(argumentKey) ?: SubscriptionSection.ALL
}
private val model by lazy {
val model by lazy {
when(subscriptionSection) {
ALL -> ViewModelProviders.of(this).get(AllSubscriptionsVM::class.java)
UNREAD -> ViewModelProviders.of(this).get(UnreadSubscriptionsVM::class.java)
......
......@@ -44,8 +44,8 @@ object Store {
task {
FreshRSSApplication.database.let {db ->
db.getAllSubcriptionsWithImageToUpdate()
.blockingFirst()
.forEach {sub -> db.insertSubscriptionImage(sub.id, sub.fetchImage())}
.blockingFirst()
.forEach {sub -> db.insertSubscriptionImage(sub.id, sub.fetchImage())}
}
}
}.toSuccessVoid()
......@@ -54,18 +54,16 @@ object Store {
fun getUnreadCount(): Promise<Unit, Exception> =
api.getUnreadCount() then {
totalUnreadCount.postValue(it.max)
it.unreadcounts.forEach {self ->
FreshRSSApplication.database.updateSubscriptionCount(self.id, self.count)
}
it.unreadcounts.forEach {self -> FreshRSSApplication.database.updateSubscriptionCount(self.id, self.count)}
}
fun getStreamItems(id: String): Promise<Unit, Exception> =
api.getStreamItems(id) then {this.e("::getStreamItems: TODO"); Unit}
fun getStreamContents(id: String): Promise<Unit, Exception> =
api.getStreamContents(id) bind {
val insertPromises = it.items.map {item ->
task {FreshRSSDabatabase.instance.insertArticle(Article.fromContentItem(item))}
task {
FreshRSSApplication.database.insertArticle(Article.fromContentItem(item))
FreshRSSApplication.database.updateSubscriptionNewestArticleDate(id, item.crawled)
}
}
all(insertPromises, cancelOthersOnError = false).toSuccessVoid()
}
......
......@@ -6,7 +6,6 @@ import fr.chenry.android.freshrss.store.database.models.*
import fr.chenry.android.freshrss.utils.*
import nl.komponents.kovenant.*
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.ui.successUi
class Api(private val account: Account) {
private val endpoints = Endpoints(account.serverInstance)
......@@ -29,12 +28,6 @@ class Api(private val account: Account) {
.authentify(this.account)
.promise()
fun getStreamItems(id: String): Promise<String, Exception> =
Fuel
.getJson(endpoints.streamItemsEndpoint, listOf("s" to id))
.authentify(this.account)
.promiseString() successUi {this.e("::getStreamItems: TODO")}
fun getStreamContents(id: String): Promise<ContentItemsHandler, Exception> =
Fuel
.getJson(endpoints.streamContentsEndpoint(id))
......
......@@ -8,7 +8,6 @@ class Endpoints(base: String) {
val tokenEndpoint = "${this.base}/reader/api/0/token"
val subscriptionEndpoint = "${this.base}/reader/api/0/subscription/list"
val unreadCountEndpoint = "${this.base}/reader/api/0/unread-count"
val streamItemsEndpoint = "${this.base}/reader/api/0/stream/items/ids"
fun streamContentsEndpoint(id: String) = "${this.base}/reader/api/0/stream/contents/$id"
val tagEndpoint = "${this.base}/reader/api/0/tag/list"
val unreadItemsEndpoint = "${this.base}/reader/api/0/stream/contents/user/-/state/com.google/reading-list"
......
package fr.chenry.android.freshrss.store.api.models
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import org.joda.time.LocalDateTime
data class ContentItemsHandler(
val id: String,
val updated: Long,
......@@ -11,9 +15,13 @@ typealias ContentItems = List<ContentItem>
data class ContentItem(
val id: String,
val crawlTimeMsec: Long,
val timestampUsec: Long,
val published: Long,
@JsonProperty("crawlTimeMsec")
@JsonDeserialize(using = MilliSecTimestampDeserializer::class)
val crawled: LocalDateTime,
@JsonProperty("timestampUsec")
@JsonDeserialize(using = MicroSecTimestampDeserializer::class)
val timestamp: LocalDateTime,
val published: LocalDateTime,
val title: String,
val alternate: List<Href>,
val categories: List<String>,
......
package fr.chenry.android.freshrss.store.api.models
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.datatype.joda.deser.LocalDateTimeDeserializer
import org.joda.time.DateTimeZone
import org.joda.time.LocalDateTime
class MicroSecTimestampDeserializer: LocalDateTimeDeserializer() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): LocalDateTime {
val tz = if(_format.isTimezoneExplicit) _format.timeZone else DateTimeZone.forTimeZone(ctxt?.timeZone)
return LocalDateTime(p?.valueAsString?.toLongOrNull()?.div(1000) ?: 0, tz)
}
}
class MilliSecTimestampDeserializer: LocalDateTimeDeserializer() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): LocalDateTime {
val tz = if(_format.isTimezoneExplicit) _format.timeZone else DateTimeZone.forTimeZone(ctxt?.timeZone)
return LocalDateTime(p?.valueAsString?.toLongOrNull() ?: 0, tz)
}
}
\ No newline at end of file
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.datatype.joda.deser.LocalDateTimeDeserializer
import org.joda.time.DateTimeZone
import org.joda.time.LocalDateTime
data class UnreadCountsHandler(val max: Int, val unreadcounts: List<UnreadCount>)
data class UnreadCount(
val id: String,
val count: Int,
@JsonDeserialize(using = MicroSecTimestampDeserializer::class)
val newestItemTimestampUsec: LocalDateTime = LocalDateTime.now()
)
class MicroSecTimestampDeserializer: LocalDateTimeDeserializer() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): LocalDateTime {
val tz = if(_format.isTimezoneExplicit) _format.timeZone else DateTimeZone.forTimeZone(ctxt?.timeZone)
return LocalDateTime(p?.valueAsString?.toLongOrNull()?.div(1000), tz)
}
}
\ No newline at end of file
val count: Int
)
\ No newline at end of file
......@@ -5,8 +5,9 @@ import android.graphics.BitmapFactory
import androidx.room.TypeConverter
import fr.chenry.android.freshrss.store.database.models.ReadStatus
import fr.chenry.android.freshrss.store.database.models.ReadStatus.READ
import fr.chenry.android.freshrss.utils.escapeHtml4
import fr.chenry.android.freshrss.utils.unescapeHtml4
import fr.chenry.android.freshrss.utils.*
import org.joda.time.DateTimeZone
import org.joda.time.LocalDateTime
import java.io.ByteArrayOutputStream
class Converters {
......@@ -36,4 +37,10 @@ class Converters {
@TypeConverter
fun blobToBitmap(byteArray: ByteArray?) =
if(byteArray?.isEmpty() != false) null else BitmapFactory.decodeStream(byteArray.inputStream())
@TypeConverter
fun localDateTimeToLong(localDateTime: LocalDateTime) = localDateTime.toDateTime(DateTimeZone.UTC).millis
@TypeConverter
fun longToLocalDateTime(epoch: Long) = Try{LocalDateTime(epoch)}.getOrDefault(LocalDateTime(0))
}
......@@ -11,9 +11,10 @@ import fr.chenry.android.freshrss.utils.Try
import fr.chenry.android.freshrss.utils.getOrDefault
import nl.komponents.kovenant.*
import nl.komponents.kovenant.functional.bind
import org.joda.time.LocalDateTime
import kotlin.reflect.KProperty
@Database(version = 3, entities = [Account::class, Article::class, Subscription::class])
@Database(version = 4, entities = [Account::class, Article::class, Subscription::class])
@TypeConverters(Converters::class)
@GenerateRoomMigrations
abstract class FreshRSSDabatabase: RoomDatabase() {
......@@ -61,10 +62,13 @@ abstract class FreshRSSDabatabase: RoomDatabase() {
}.toSuccessVoid()
fun updateSubscriptionCount(id: String, count: Int) = getSubscriptionsDAO().updateCount(id, count)
fun updateSubscriptionNewestArticleDate(id: String, newestArticleDate: LocalDateTime) =
getSubscriptionsDAO().updateNewestArticleDate(id, newestArticleDate)
fun incrementSubscriptionCount(id: String) = getSubscriptionsDAO().incrementCount(id)
fun decrementSubscriptionCount(id: String) = getSubscriptionsDAO().decrementCount(id)
fun insertSubscriptionImage(id: String, bitmap: Bitmap) = getSubscriptionsDAO().insertImage(id, bitmap)
fun getAllSubcriptions() = getSubscriptionsDAO().getAll()
fun getAllUnreadSubcriptions() = getSubscriptionsDAO().getAllUnread()
fun getAllSubcriptionsIds() = getSubscriptionsDAO().getAllIds()
fun getAllSubcriptionsWithImageToUpdate() = getSubscriptionsDAO().withImageToUpdate()
......
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "ebcce02af6cef8a0085a6b22d710c6cb",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `SID` TEXT NOT NULL, `Auth` TEXT NOT NULL, `login` TEXT NOT NULL, `serverInstance` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "SID",
"columnName": "SID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "Auth",
"columnName": "Auth",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "login",
"columnName": "login",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverInstance",
"columnName": "serverInstance",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "articles",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `href` TEXT NOT NULL, `categories` TEXT NOT NULL, `author` TEXT NOT NULL, `content` TEXT NOT NULL, `streamId` TEXT NOT NULL, `readStatus` TEXT NOT NULL, `crawled` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "href",
"columnName": "href",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "categories",
"columnName": "categories",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "streamId",
"columnName": "streamId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "readStatus",
"columnName": "readStatus",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "crawled",
"columnName": "crawled",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "subscriptions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`imageBitmap` BLOB, `id` TEXT NOT NULL, `title` TEXT NOT NULL, `iconUrl` TEXT NOT NULL, `unreadCount` INTEGER NOT NULL, `newestArticleDate` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "imageBitmap",
"columnName": "imageBitmap",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "iconUrl",
"columnName": "iconUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unreadCount",
"columnName": "unreadCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newestArticleDate",
"columnName": "newestArticleDate",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"ebcce02af6cef8a0085a6b22d710c6cb\")"
]
}
}
\ No newline at end of file
......@@ -3,6 +3,7 @@ package fr.chenry.android.freshrss.store.database.models
import android.net.Uri
import android.os.Parcelable
import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import androidx.lifecycle.LiveData
import androidx.room.*
import fr.chenry.android.freshrss.store.api.models.ContentItem
......@@ -10,7 +11,7 @@ import fr.chenry.android.freshrss.store.api.models.StreamId
import fr.chenry.android.freshrss.utils.Try
import io.reactivex.Flowable
import kotlinx.android.parcel.Parcelize
import org.joda.time.LocalDateTime
typealias ItemId = String
typealias Articles = List<Article>
......@@ -25,7 +26,8 @@ data class Article(
val author: String,
val content: String,
val streamId: StreamId,
val readStatus: ReadStatus = ReadStatus.READ
val readStatus: ReadStatus = ReadStatus.READ,
val crawled: LocalDateTime = LocalDateTime(0)
): BaseObservable() {
@Ignore
val url = Try{Uri.parse(href)}.getOrNull()
......@@ -38,7 +40,8 @@ data class Article(
item.categories,
item.author,
item.summary.content,
item.origin.streamId
item.origin.streamId,
crawled = item.crawled
)
}
}
......
......@@ -22,11 +22,9 @@ data class Subscription(
val id: String,
val title: String,
val iconUrl: String,
val unreadCount: Int = 0
val unreadCount: Int = 0,
val newestArticleDate: LocalDateTime = LocalDateTime(0)
): BaseObservable() {
@Ignore
var newestItemDate: LocalDateTime = LocalDateTime(0)
@get:Bindable
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var imageBitmap: Bitmap? = null
......@@ -42,7 +40,7 @@ data class Subscription(
companion object {
fun fromSubscriptionApiItem(subscriptionApiItem: SubscriptionApiItem) = Subscription(
subscriptionApiItem.id,
subscriptionApiItem.title,
subscriptionApiItem.title.trim(),
subscriptionApiItem.iconUrl
)
}
......@@ -74,6 +72,9 @@ interface SubscriptionsDAO {
@Query("SELECT * FROM subscriptions")
fun getAll(): Flowable<Subscriptions>
@Query("SELECT * FROM subscriptions WHERE unreadCount > 0")
fun getAllUnread(): Flowable<Subscriptions>
@Query("SELECT * FROM subscriptions WHERE id = :id")
fun byId(id: String): Flowable<Subscription>
......@@ -86,6 +87,9 @@ interface SubscriptionsDAO {
@Query("UPDATE subscriptions SET unreadCount = :count WHERE id = :id")
fun updateCount(id: String, count: Int)
@Query("UPDATE subscriptions SET newestArticleDate = :newestArticleDate WHERE id = :id AND newestArticleDate < :newestArticleDate")
fun updateNewestArticleDate(id: String, newestArticleDate: LocalDateTime)
@Query("UPDATE subscriptions SET unreadCount = unreadCount + 1 WHERE id = :id")
fun incrementCount(id: String)
......
......@@ -2,29 +2,69 @@ package fr.chenry.android.freshrss.store.viewmodels
import androidx.lifecycle.*
import fr.chenry.android.freshrss.FreshRSSApplication
import fr.chenry.android.freshrss.store.database.models.Subscription
import fr.chenry.android.freshrss.store.database.models.Subscriptions
import fr.chenry.android.freshrss.R
import fr.chenry.android.freshrss.store.Store
import fr.chenry.android.freshrss.utils.e
import fr.chenry.android.freshrss.utils.whenNull
import nl.komponents.kovenant.task
import org.joda.time.LocalDateTime
sealed class SubscriptionsVM: ViewModel() {
protected val subscriptionsLiveData = FreshRSSApplication.database.getAllSubcriptions().toLiveData()
val liveData: LiveData<Subscriptions> = MutableLiveData<Subscriptions>().apply {
value = load() ?: listOf()
subscriptionsLiveData.observeForever {value = load() ?: listOf()}
abstract val subscriptionsLiveData: LiveData<Subscriptions>
val liveData: LiveData<Subscriptions> by lazy {
MutableLiveData<Subscriptions>().apply {
value = load()
subscriptionsLiveData.observeForever {value = load()}
}
}
val sectionnedLiveData: LiveData<Map<String, Subscriptions>> by lazy {
MutableLiveData<Map<String, Subscriptions>>().apply {
liveData.observeForever {value = groupBy(it)}
}
}
protected abstract fun load(): Subscriptions?
protected open fun groupBy(subscriptions: Subscriptions): Map<String, Subscriptions> =
subscriptions.groupBy {it.title[0].toString().toUpperCase()}
protected open fun load(): Subscriptions = subscriptionsLiveData.value?.sortedWith(Comparator {o1, o2 ->
o1.title.compareTo(o2.title, true)
}) ?: listOf()
}
class AllSubscriptionsVM: SubscriptionsVM() {
override fun load() = subscriptionsLiveData.value?.sortedWith(Comparator {o1, o2 ->
o1.title.compareTo(o2.title, true)
})
override val subscriptionsLiveData = FreshRSSApplication.database.getAllSubcriptions().toLiveData()
}
class UnreadSubscriptionsVM: SubscriptionsVM() {
override fun load() = subscriptionsLiveData.value?.filter {it.unreadCount > 0}?.sortedBy {it.newestItemDate}
override val subscriptionsLiveData = FreshRSSApplication.database.getAllUnreadSubcriptions().toLiveData()
override fun groupBy(subscriptions: Subscriptions): Map<String, Subscriptions> = subscriptions.groupBy {
val today = LocalDateTime.now().withTime(0, 0, 0, 0)
when {
it.newestArticleDate.isAfter(today.minusDays(1)) -> R.string.human_time_grouping_today
it.newestArticleDate.isAfter(today.minusDays(2)) -> R.string.human_time_grouping_yesterday
it.newestArticleDate.isAfter(today.withDayOfWeek(1)) -> R.string.human_time_grouping_this_week
it.newestArticleDate.isAfter(today.withDayOfWeek(1).minusWeeks(1)) -> R.string.human_time_grouping_last_week
it.newestArticleDate.isAfter(today.withDayOfMonth(1)) -> R.string.human_time_grouping_this_month
it.newestArticleDate.isAfter(today.withDayOfMonth(1).minusMonths(1)) -> R.string.human_time_grouping_last_month
it.newestArticleDate.isAfter(today.withDayOfYear(1)) -> R.string.human_time_grouping_this_year
else -> R.string.human_time_grouping_old_articles
}.let {id -> FreshRSSApplication.getStringR(id)}
}
override fun load(): Subscriptions = subscriptionsLiveData.value?.sortedWith(Comparator {o1, o2 ->
if(o1.newestArticleDate.isEqual(o2.newestArticleDate))
o1.title.compareTo(o2.title, true) else
o2.newestArticleDate.compareTo(o1.newestArticleDate)
}) ?: listOf()
}
class FavoritesSubscriptionsVM: SubscriptionsVM() {
override fun load() = listOf<Subscription>()
override val subscriptionsLiveData: LiveData<Subscriptions> = MutableLiveData<Subscriptions>()
.apply {value = listOf()}
override fun groupBy(subscriptions: Subscriptions): Map<String, Subscriptions> = mapOf()
}
\ No newline at end of file
......@@ -10,16 +10,16 @@
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/subcription_pull_to_refresh"
tools:context=".components.subscriptions.SubscriptionsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toTopOf="@+id/subcription_bottom_navigation"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context=".components.subscriptions.SubscriptionsFragment">
app:layout_constraintBottom_toTopOf="@id/subcription_bottom_navigation">
<androidx.viewpager.widget.ViewPager
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:id="@+id/subcription_fragment_container" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>