2022-01-11 19:00:29 +01:00
|
|
|
/* Copyright 2021 Tusky Contributors
|
|
|
|
*
|
|
|
|
* This file is a part of Tusky.
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
|
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
|
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
|
|
* Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
|
|
* see <http://www.gnu.org/licenses>. */
|
|
|
|
|
|
|
|
package com.keylesspalace.tusky.components.timeline.viewmodel
|
|
|
|
|
|
|
|
import android.content.SharedPreferences
|
|
|
|
import android.util.Log
|
|
|
|
import androidx.lifecycle.viewModelScope
|
|
|
|
import androidx.paging.ExperimentalPagingApi
|
|
|
|
import androidx.paging.Pager
|
|
|
|
import androidx.paging.PagingConfig
|
|
|
|
import androidx.paging.cachedIn
|
|
|
|
import androidx.paging.filter
|
|
|
|
import androidx.paging.map
|
|
|
|
import androidx.room.withTransaction
|
|
|
|
import com.google.gson.Gson
|
|
|
|
import com.keylesspalace.tusky.appstore.BookmarkEvent
|
|
|
|
import com.keylesspalace.tusky.appstore.EventHub
|
|
|
|
import com.keylesspalace.tusky.appstore.FavoriteEvent
|
|
|
|
import com.keylesspalace.tusky.appstore.PinEvent
|
|
|
|
import com.keylesspalace.tusky.appstore.ReblogEvent
|
|
|
|
import com.keylesspalace.tusky.components.timeline.Placeholder
|
|
|
|
import com.keylesspalace.tusky.components.timeline.toEntity
|
|
|
|
import com.keylesspalace.tusky.components.timeline.toViewData
|
|
|
|
import com.keylesspalace.tusky.db.AccountManager
|
|
|
|
import com.keylesspalace.tusky.db.AppDatabase
|
|
|
|
import com.keylesspalace.tusky.entity.Poll
|
|
|
|
import com.keylesspalace.tusky.network.FilterModel
|
|
|
|
import com.keylesspalace.tusky.network.MastodonApi
|
|
|
|
import com.keylesspalace.tusky.network.TimelineCases
|
|
|
|
import com.keylesspalace.tusky.util.dec
|
|
|
|
import com.keylesspalace.tusky.util.inc
|
|
|
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
2022-02-21 19:33:10 +01:00
|
|
|
import kotlinx.coroutines.delay
|
2022-01-11 19:00:29 +01:00
|
|
|
import kotlinx.coroutines.flow.map
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
import kotlinx.coroutines.rx3.await
|
|
|
|
import retrofit2.HttpException
|
|
|
|
import javax.inject.Inject
|
2022-02-21 19:33:10 +01:00
|
|
|
import kotlin.time.DurationUnit
|
|
|
|
import kotlin.time.toDuration
|
2022-01-11 19:00:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* TimelineViewModel that caches all statuses in a local database
|
|
|
|
*/
|
|
|
|
class CachedTimelineViewModel @Inject constructor(
|
|
|
|
timelineCases: TimelineCases,
|
|
|
|
private val api: MastodonApi,
|
|
|
|
eventHub: EventHub,
|
|
|
|
accountManager: AccountManager,
|
|
|
|
sharedPreferences: SharedPreferences,
|
|
|
|
filterModel: FilterModel,
|
|
|
|
private val db: AppDatabase,
|
|
|
|
private val gson: Gson
|
|
|
|
) : TimelineViewModel(timelineCases, api, eventHub, accountManager, sharedPreferences, filterModel) {
|
|
|
|
|
2022-01-20 21:10:32 +01:00
|
|
|
@OptIn(ExperimentalPagingApi::class)
|
2022-01-11 19:00:29 +01:00
|
|
|
override val statuses = Pager(
|
|
|
|
config = PagingConfig(pageSize = LOAD_AT_ONCE),
|
|
|
|
remoteMediator = CachedTimelineRemoteMediator(accountManager, api, db, gson),
|
2022-03-06 17:40:24 +01:00
|
|
|
pagingSourceFactory = {
|
|
|
|
val activeAccount = accountManager.activeAccount
|
|
|
|
if (activeAccount == null) {
|
|
|
|
EmptyTimelinePagingSource()
|
|
|
|
} else {
|
|
|
|
db.timelineDao().getStatuses(activeAccount.id)
|
|
|
|
}
|
|
|
|
}
|
2022-01-11 19:00:29 +01:00
|
|
|
).flow
|
|
|
|
.map { pagingData ->
|
|
|
|
pagingData.map { timelineStatus ->
|
|
|
|
timelineStatus.toViewData(gson)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.map { pagingData ->
|
|
|
|
pagingData.filter { statusViewData ->
|
|
|
|
!shouldFilterStatus(statusViewData)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.cachedIn(viewModelScope)
|
|
|
|
|
2022-02-21 19:33:10 +01:00
|
|
|
init {
|
|
|
|
viewModelScope.launch {
|
|
|
|
delay(5.toDuration(DurationUnit.SECONDS)) // delay so the db is not locked during initial ui refresh
|
|
|
|
accountManager.activeAccount?.id?.let { accountId ->
|
|
|
|
db.timelineDao().cleanup(accountId, MAX_STATUSES_IN_CACHE)
|
|
|
|
db.timelineDao().cleanupAccounts(accountId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-11 19:00:29 +01:00
|
|
|
override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) {
|
|
|
|
// handled by CacheUpdater
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun changeExpanded(expanded: Boolean, status: StatusViewData.Concrete) {
|
|
|
|
viewModelScope.launch {
|
|
|
|
db.timelineDao().setExpanded(accountManager.activeAccount!!.id, status.id, expanded)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) {
|
|
|
|
viewModelScope.launch {
|
|
|
|
db.timelineDao().setContentShowing(accountManager.activeAccount!!.id, status.id, isShowing)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) {
|
|
|
|
viewModelScope.launch {
|
|
|
|
db.timelineDao().setContentCollapsed(accountManager.activeAccount!!.id, status.id, isCollapsed)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun removeAllByAccountId(accountId: String) {
|
|
|
|
viewModelScope.launch {
|
|
|
|
db.timelineDao().removeAllByUser(accountManager.activeAccount!!.id, accountId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun removeAllByInstance(instance: String) {
|
|
|
|
viewModelScope.launch {
|
|
|
|
db.timelineDao().deleteAllFromInstance(accountManager.activeAccount!!.id, instance)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun removeStatusWithId(id: String) {
|
|
|
|
// handled by CacheUpdater
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun loadMore(placeholderId: String) {
|
|
|
|
viewModelScope.launch {
|
|
|
|
try {
|
|
|
|
val timelineDao = db.timelineDao()
|
|
|
|
|
|
|
|
val activeAccount = accountManager.activeAccount!!
|
|
|
|
|
|
|
|
timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id))
|
|
|
|
|
2022-02-03 18:51:15 +01:00
|
|
|
val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
|
|
|
|
2022-03-01 21:29:05 +01:00
|
|
|
val response = api.homeTimeline(maxId = placeholderId.inc(), sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE).await()
|
2022-01-11 19:00:29 +01:00
|
|
|
|
|
|
|
val statuses = response.body()
|
|
|
|
if (!response.isSuccessful || statuses == null) {
|
|
|
|
loadMoreFailed(placeholderId, HttpException(response))
|
|
|
|
return@launch
|
|
|
|
}
|
|
|
|
|
|
|
|
db.withTransaction {
|
|
|
|
|
|
|
|
timelineDao.delete(activeAccount.id, placeholderId)
|
|
|
|
|
|
|
|
val overlappedStatuses = if (statuses.isNotEmpty()) {
|
|
|
|
timelineDao.deleteRange(activeAccount.id, statuses.last().id, statuses.first().id)
|
|
|
|
} else {
|
|
|
|
0
|
|
|
|
}
|
|
|
|
|
|
|
|
for (status in statuses) {
|
|
|
|
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
|
|
|
status.reblog?.account?.toEntity(activeAccount.id, gson)?.let { rebloggedAccount ->
|
|
|
|
timelineDao.insertAccount(rebloggedAccount)
|
|
|
|
}
|
|
|
|
timelineDao.insertStatus(
|
|
|
|
status.toEntity(
|
|
|
|
timelineUserId = activeAccount.id,
|
|
|
|
gson = gson,
|
|
|
|
expanded = activeAccount.alwaysOpenSpoiler,
|
|
|
|
contentShowing = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
|
|
|
contentCollapsed = true
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-02-02 18:29:59 +01:00
|
|
|
if (overlappedStatuses == 0 && statuses.isNotEmpty()) {
|
2022-01-11 19:00:29 +01:00
|
|
|
timelineDao.insertStatus(
|
|
|
|
Placeholder(statuses.last().id.dec(), loading = false).toEntity(activeAccount.id)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2022-02-02 18:29:59 +01:00
|
|
|
} catch (e: Exception) {
|
2022-01-11 19:00:29 +01:00
|
|
|
loadMoreFailed(placeholderId, e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private suspend fun loadMoreFailed(placeholderId: String, e: Exception) {
|
|
|
|
Log.w("CachedTimelineVM", "failed loading statuses", e)
|
|
|
|
val activeAccount = accountManager.activeAccount!!
|
|
|
|
db.timelineDao().insertStatus(Placeholder(placeholderId, loading = false).toEntity(activeAccount.id))
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun handleReblogEvent(reblogEvent: ReblogEvent) {
|
|
|
|
// handled by CacheUpdater
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun handleFavEvent(favEvent: FavoriteEvent) {
|
|
|
|
// handled by CacheUpdater
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun handleBookmarkEvent(bookmarkEvent: BookmarkEvent) {
|
|
|
|
// handled by CacheUpdater
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun handlePinEvent(pinEvent: PinEvent) {
|
|
|
|
// handled by CacheUpdater
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun fullReload() {
|
|
|
|
viewModelScope.launch {
|
|
|
|
val activeAccount = accountManager.activeAccount!!
|
2022-03-02 20:40:06 +01:00
|
|
|
db.timelineDao().removeAll(activeAccount.id)
|
2022-01-11 19:00:29 +01:00
|
|
|
}
|
|
|
|
}
|
2022-02-21 19:33:10 +01:00
|
|
|
|
|
|
|
companion object {
|
|
|
|
private const val MAX_STATUSES_IN_CACHE = 1000
|
|
|
|
}
|
2022-01-11 19:00:29 +01:00
|
|
|
}
|