Loading of statuses and loading of filters is an "intended" race: we want to display statuses first, especially if they are cached. Unfortunately we do not cache filters themselves so when we load cached statuses we do not apply filters. One part of the solution is to re-filter the statuses once we fetch the filters. This commit implements it. Caching of filters is not included yet.
This commit is contained in:
parent
579f0eb833
commit
e63cd68baf
3 changed files with 59 additions and 12 deletions
|
@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.ExperimentalPagingApi
|
import androidx.paging.ExperimentalPagingApi
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import androidx.paging.filter
|
import androidx.paging.filter
|
||||||
import androidx.paging.map
|
import androidx.paging.map
|
||||||
|
@ -37,6 +38,7 @@ import com.keylesspalace.tusky.components.timeline.toViewData
|
||||||
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
@ -66,7 +68,16 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
filterModel: FilterModel,
|
filterModel: FilterModel,
|
||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
private val gson: Gson
|
private val gson: Gson
|
||||||
) : TimelineViewModel(timelineCases, api, eventHub, accountManager, sharedPreferences, filterModel) {
|
) : TimelineViewModel(
|
||||||
|
timelineCases,
|
||||||
|
api,
|
||||||
|
eventHub,
|
||||||
|
accountManager,
|
||||||
|
sharedPreferences,
|
||||||
|
filterModel
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var currentPagingSource: PagingSource<Int, TimelineStatusWithAccount>? = null
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
override val statuses = Pager(
|
override val statuses = Pager(
|
||||||
|
@ -78,6 +89,8 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
EmptyTimelinePagingSource()
|
EmptyTimelinePagingSource()
|
||||||
} else {
|
} else {
|
||||||
db.timelineDao().getStatuses(activeAccount.id)
|
db.timelineDao().getStatuses(activeAccount.id)
|
||||||
|
}.also { newPagingSource ->
|
||||||
|
this.currentPagingSource = newPagingSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).flow
|
).flow
|
||||||
|
@ -113,13 +126,15 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
|
|
||||||
override fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) {
|
override fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
db.timelineDao().setContentShowing(accountManager.activeAccount!!.id, status.id, isShowing)
|
db.timelineDao()
|
||||||
|
.setContentShowing(accountManager.activeAccount!!.id, status.id, isShowing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) {
|
override fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
db.timelineDao().setContentCollapsed(accountManager.activeAccount!!.id, status.id, isCollapsed)
|
db.timelineDao()
|
||||||
|
.setContentCollapsed(accountManager.activeAccount!!.id, status.id, isCollapsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,12 +161,21 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
|
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
|
|
||||||
timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id))
|
timelineDao.insertStatus(
|
||||||
|
Placeholder(placeholderId, loading = true).toEntity(
|
||||||
|
activeAccount.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val response = db.withTransaction {
|
val response = db.withTransaction {
|
||||||
val idAbovePlaceholder = timelineDao.getIdAbove(activeAccount.id, placeholderId)
|
val idAbovePlaceholder = timelineDao.getIdAbove(activeAccount.id, placeholderId)
|
||||||
val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
val nextPlaceholderId =
|
||||||
api.homeTimeline(maxId = idAbovePlaceholder, sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE)
|
timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
||||||
|
api.homeTimeline(
|
||||||
|
maxId = idAbovePlaceholder,
|
||||||
|
sinceId = nextPlaceholderId,
|
||||||
|
limit = LOAD_AT_ONCE
|
||||||
|
)
|
||||||
}.await()
|
}.await()
|
||||||
|
|
||||||
val statuses = response.body()
|
val statuses = response.body()
|
||||||
|
@ -165,14 +189,19 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
timelineDao.delete(activeAccount.id, placeholderId)
|
timelineDao.delete(activeAccount.id, placeholderId)
|
||||||
|
|
||||||
val overlappedStatuses = if (statuses.isNotEmpty()) {
|
val overlappedStatuses = if (statuses.isNotEmpty()) {
|
||||||
timelineDao.deleteRange(activeAccount.id, statuses.last().id, statuses.first().id)
|
timelineDao.deleteRange(
|
||||||
|
activeAccount.id,
|
||||||
|
statuses.last().id,
|
||||||
|
statuses.first().id
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
for (status in statuses) {
|
for (status in statuses) {
|
||||||
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
||||||
status.reblog?.account?.toEntity(activeAccount.id, gson)?.let { rebloggedAccount ->
|
status.reblog?.account?.toEntity(activeAccount.id, gson)
|
||||||
|
?.let { rebloggedAccount ->
|
||||||
timelineDao.insertAccount(rebloggedAccount)
|
timelineDao.insertAccount(rebloggedAccount)
|
||||||
}
|
}
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
|
@ -193,7 +222,10 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
to guarantee the placeholder has an id that exists on the server as not all
|
to guarantee the placeholder has an id that exists on the server as not all
|
||||||
servers handle client generated ids as expected */
|
servers handle client generated ids as expected */
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id)
|
Placeholder(
|
||||||
|
statuses.last().id,
|
||||||
|
loading = false
|
||||||
|
).toEntity(activeAccount.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,7 +240,8 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
private suspend fun loadMoreFailed(placeholderId: String, e: Exception) {
|
private suspend fun loadMoreFailed(placeholderId: String, e: Exception) {
|
||||||
Log.w("CachedTimelineVM", "failed loading statuses", e)
|
Log.w("CachedTimelineVM", "failed loading statuses", e)
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
db.timelineDao().insertStatus(Placeholder(placeholderId, loading = false).toEntity(activeAccount.id))
|
db.timelineDao()
|
||||||
|
.insertStatus(Placeholder(placeholderId, loading = false).toEntity(activeAccount.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleReblogEvent(reblogEvent: ReblogEvent) {
|
override fun handleReblogEvent(reblogEvent: ReblogEvent) {
|
||||||
|
@ -234,6 +267,10 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun invalidate() {
|
||||||
|
currentPagingSource?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_STATUSES_IN_CACHE = 1000
|
private const val MAX_STATUSES_IN_CACHE = 1000
|
||||||
}
|
}
|
||||||
|
|
|
@ -249,6 +249,10 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
currentSource?.invalidate()
|
currentSource?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun invalidate() {
|
||||||
|
currentSource?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, HttpException::class)
|
@Throws(IOException::class, HttpException::class)
|
||||||
suspend fun fetchStatusesForKind(
|
suspend fun fetchStatusesForKind(
|
||||||
fromId: String?,
|
fromId: String?,
|
||||||
|
|
|
@ -173,6 +173,9 @@ abstract class TimelineViewModel(
|
||||||
|
|
||||||
abstract fun fullReload()
|
abstract fun fullReload()
|
||||||
|
|
||||||
|
/** Triggered when currently displayed data must be reloaded. */
|
||||||
|
protected abstract fun invalidate()
|
||||||
|
|
||||||
protected fun shouldFilterStatus(statusViewData: StatusViewData): Boolean {
|
protected fun shouldFilterStatus(statusViewData: StatusViewData): Boolean {
|
||||||
val status = statusViewData.asStatusOrNull()?.status ?: return false
|
val status = statusViewData.asStatusOrNull()?.status ?: return false
|
||||||
return status.inReplyToId != null && filterRemoveReplies ||
|
return status.inReplyToId != null && filterRemoveReplies ||
|
||||||
|
@ -288,6 +291,9 @@ abstract class TimelineViewModel(
|
||||||
filterContextMatchesKind(kind, it.context)
|
filterContextMatchesKind(kind, it.context)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
// After the filters are loaded we need to reload displayed content to apply them.
|
||||||
|
// It can happen during the usage or at startup, when we get statuses before filters.
|
||||||
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue