Format TimelineRepository.kt

This commit is contained in:
Adolfo Santiago 2022-04-03 10:24:46 +02:00
parent 7047eb67aa
commit 69f27b92e5
No known key found for this signature in database
GPG key ID: 244D6F9A317B4A65

View file

@ -19,7 +19,6 @@ import io.reactivex.schedulers.Schedulers
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList
data class Placeholder(val id: String) data class Placeholder(val id: String)
@ -30,8 +29,10 @@ enum class TimelineRequestMode {
} }
interface TimelineRepository { interface TimelineRepository {
fun getStatuses(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, fun getStatuses(
requestMode: TimelineRequestMode): Single<out List<TimelineStatus>> maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int,
requestMode: TimelineRequestMode
): Single<out List<TimelineStatus>>
companion object { companion object {
val CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(14) val CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(14)
@ -39,104 +40,110 @@ interface TimelineRepository {
} }
class TimelineRepositoryImpl( class TimelineRepositoryImpl(
private val timelineDao: TimelineDao, private val timelineDao: TimelineDao,
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
private val accountManager: AccountManager, private val accountManager: AccountManager,
private val gson: Gson private val gson: Gson
) : TimelineRepository { ) : TimelineRepository {
init { init {
this.cleanup() this.cleanup()
} }
override fun getStatuses(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, override fun getStatuses(
limit: Int, requestMode: TimelineRequestMode maxId: String?, sinceId: String?, sincedIdMinusOne: String?,
limit: Int, requestMode: TimelineRequestMode
): Single<out List<TimelineStatus>> { ): Single<out List<TimelineStatus>> {
val acc = accountManager.activeAccount ?: throw IllegalStateException() val acc = accountManager.activeAccount ?: throw IllegalStateException()
val accountId = acc.id val accountId = acc.id
return if (requestMode == DISK) { return if(requestMode == DISK) {
this.getStatusesFromDb(accountId, maxId, sinceId, limit) this.getStatusesFromDb(accountId, maxId, sinceId, limit)
} else { } else {
getStatusesFromNetwork(maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode) getStatusesFromNetwork(maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode)
} }
} }
private fun getStatusesFromNetwork(maxId: String?, sinceId: String?, private fun getStatusesFromNetwork(
sinceIdMinusOne: String?, limit: Int, maxId: String?, sinceId: String?,
accountId: Long, requestMode: TimelineRequestMode sinceIdMinusOne: String?, limit: Int,
accountId: Long, requestMode: TimelineRequestMode
): Single<out List<TimelineStatus>> { ): Single<out List<TimelineStatus>> {
return mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1) return mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1)
.map { statuses -> .map { statuses ->
this.saveStatusesToDb(accountId, statuses, maxId, sinceId) this.saveStatusesToDb(accountId, statuses, maxId, sinceId)
} }
.flatMap { statuses -> .flatMap { statuses ->
this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode) this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode)
} }
.onErrorResumeNext { error -> .onErrorResumeNext { error ->
if (error is IOException && requestMode != NETWORK) { if(error is IOException && requestMode != NETWORK) {
this.getStatusesFromDb(accountId, maxId, sinceId, limit) this.getStatusesFromDb(accountId, maxId, sinceId, limit)
} else { } else {
Single.error(error) Single.error(error)
}
} }
}
} }
private fun addFromDbIfNeeded(accountId: Long, statuses: List<Either<Placeholder, Status>>, private fun addFromDbIfNeeded(
maxId: String?, sinceId: String?, limit: Int, accountId: Long, statuses: List<Either<Placeholder, Status>>,
requestMode: TimelineRequestMode maxId: String?, sinceId: String?, limit: Int,
requestMode: TimelineRequestMode
): Single<List<TimelineStatus>>? { ): Single<List<TimelineStatus>>? {
return if (requestMode != NETWORK && statuses.size < 2) { return if(requestMode != NETWORK && statuses.size < 2) {
val newMaxID = if (statuses.isEmpty()) { val newMaxID = if(statuses.isEmpty()) {
maxId maxId
} else { } else {
statuses.last { it.isRight() }.asRight().id statuses.last { it.isRight() }.asRight().id
} }
this.getStatusesFromDb(accountId, newMaxID, sinceId, limit) this.getStatusesFromDb(accountId, newMaxID, sinceId, limit)
.map { fromDb -> .map { fromDb ->
// If it's just placeholders and less than limit (so we exhausted both // If it's just placeholders and less than limit (so we exhausted both
// db and server at this point) // db and server at this point)
if (fromDb.size < limit && fromDb.all { !it.isRight() }) { if(fromDb.size < limit && fromDb.all { !it.isRight() }) {
statuses statuses
} else { } else {
statuses + fromDb statuses + fromDb
}
} }
}
} else { } else {
Single.just(statuses) Single.just(statuses)
} }
} }
private fun getStatusesFromDb(accountId: Long, maxId: String?, sinceId: String?, private fun getStatusesFromDb(
limit: Int): Single<out List<TimelineStatus>> { accountId: Long, maxId: String?, sinceId: String?,
limit: Int
): Single<out List<TimelineStatus>> {
return timelineDao.getStatusesForAccount(accountId, maxId, sinceId, limit) return timelineDao.getStatusesForAccount(accountId, maxId, sinceId, limit)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.map { statuses -> .map { statuses ->
statuses.map { it.toStatus() } statuses.map { it.toStatus() }
} }
} }
private fun saveStatusesToDb(accountId: Long, statuses: List<Status>, private fun saveStatusesToDb(
maxId: String?, sinceId: String? accountId: Long, statuses: List<Status>,
maxId: String?, sinceId: String?
): List<Either<Placeholder, Status>> { ): List<Either<Placeholder, Status>> {
var placeholderToInsert: Placeholder? = null var placeholderToInsert: Placeholder? = null
// Look for overlap // Look for overlap
val resultStatuses = if (statuses.isNotEmpty() && sinceId != null) { val resultStatuses = if(statuses.isNotEmpty() && sinceId != null) {
val indexOfSince = statuses.indexOfLast { it.id == sinceId } val indexOfSince = statuses.indexOfLast { it.id == sinceId }
if (indexOfSince == -1) { if(indexOfSince == -1) {
// We didn't find the status which must be there. Add a placeholder // We didn't find the status which must be there. Add a placeholder
placeholderToInsert = Placeholder(sinceId.inc()) placeholderToInsert = Placeholder(sinceId.inc())
statuses.mapTo(mutableListOf(), Status::lift) statuses.mapTo(mutableListOf(), Status::lift)
.apply { .apply {
add(Either.Left(placeholderToInsert)) add(Either.Left(placeholderToInsert))
} }
} else { } else {
// There was an overlap. Remove all overlapped statuses. No need for a placeholder. // There was an overlap. Remove all overlapped statuses. No need for a placeholder.
statuses.mapTo(mutableListOf(), Status::lift) statuses.mapTo(mutableListOf(), Status::lift)
.apply { .apply {
subList(indexOfSince, size).clear() subList(indexOfSince, size).clear()
} }
} }
} else { } else {
// Just a normal case. // Just a normal case.
@ -149,11 +156,11 @@ class TimelineRepositoryImpl(
timelineDao.deleteRange(accountId, statuses.last().id, statuses.first().id) timelineDao.deleteRange(accountId, statuses.last().id, statuses.first().id)
} }
for (status in statuses) { for(status in statuses) {
timelineDao.insertInTransaction( timelineDao.insertInTransaction(
status.toEntity(accountId, gson), status.toEntity(accountId, gson),
status.account.toEntity(accountId, gson), status.account.toEntity(accountId, gson),
status.reblog?.account?.toEntity(accountId, gson) status.reblog?.account?.toEntity(accountId, gson)
) )
} }
@ -163,21 +170,24 @@ class TimelineRepositoryImpl(
// If we're loading in the bottom insert placeholder after every load // If we're loading in the bottom insert placeholder after every load
// (for requests on next launches) but not return it. // (for requests on next launches) but not return it.
if (sinceId == null && statuses.isNotEmpty()) { if(sinceId == null && statuses.isNotEmpty()) {
timelineDao.insertStatusIfNotThere( timelineDao.insertStatusIfNotThere(
Placeholder(statuses.last().id.dec()).toEntity(accountId)) Placeholder(statuses.last().id.dec()).toEntity(accountId)
)
} }
// There may be placeholders which we thought could be from our TL but they are not // There may be placeholders which we thought could be from our TL but they are not
if (statuses.size > 2) { if(statuses.size > 2) {
timelineDao.removeAllPlaceholdersBetween(accountId, statuses.first().id, timelineDao.removeAllPlaceholdersBetween(
statuses.last().id) accountId, statuses.first().id,
} else if (placeholderToInsert == null && maxId != null && sinceId != null) { statuses.last().id
)
} else if(placeholderToInsert == null && maxId != null && sinceId != null) {
timelineDao.removeAllPlaceholdersBetween(accountId, maxId, sinceId) timelineDao.removeAllPlaceholdersBetween(accountId, maxId, sinceId)
} }
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe() .subscribe()
return resultStatuses return resultStatuses
} }
@ -190,101 +200,109 @@ class TimelineRepositoryImpl(
} }
private fun TimelineStatusWithAccount.toStatus(): TimelineStatus { private fun TimelineStatusWithAccount.toStatus(): TimelineStatus {
if (this.status.authorServerId == null) { if(this.status.authorServerId == null) {
return Either.Left(Placeholder(this.status.serverId)) return Either.Left(Placeholder(this.status.serverId))
} }
val attachments: ArrayList<Attachment> = gson.fromJson(status.attachments, val attachments: ArrayList<Attachment> = gson.fromJson(
object : TypeToken<List<Attachment>>() {}.type) ?: ArrayList() status.attachments,
val mentions: Array<Status.Mention> = gson.fromJson(status.mentions, object : TypeToken<List<Attachment>>() {}.type
Array<Status.Mention>::class.java) ?: arrayOf() ) ?: ArrayList()
val mentions: Array<Status.Mention> = gson.fromJson(
status.mentions,
Array<Status.Mention>::class.java
) ?: arrayOf()
val application = gson.fromJson(status.application, Status.Application::class.java) val application = gson.fromJson(status.application, Status.Application::class.java)
val emojis: List<Emoji> = gson.fromJson(status.emojis, val emojis: List<Emoji> = gson.fromJson(
object : TypeToken<List<Emoji>>() {}.type) ?: listOf() status.emojis,
object : TypeToken<List<Emoji>>() {}.type
) ?: listOf()
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java) val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
val pleroma = gson.fromJson(status.pleroma, Status.PleromaStatus::class.java) val pleroma = gson.fromJson(status.pleroma, Status.PleromaStatus::class.java)
val reblog = status.reblogServerId?.let { id -> val reblog = status.reblogServerId?.let { id ->
Status( Status(
id = id, id = id,
url = status.url, url = status.url,
account = account.toAccount(gson), account = account.toAccount(gson),
inReplyToId = status.inReplyToId, inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId, inReplyToAccountId = status.inReplyToAccountId,
reblog = null, reblog = null,
content = status.content?.parseAsHtml()?.trimTrailingWhitespace() ?: SpannedString(""), content = status.content?.parseAsHtml()?.trimTrailingWhitespace()
createdAt = Date(status.createdAt), ?: SpannedString(""),
emojis = emojis, createdAt = Date(status.createdAt),
reblogsCount = status.reblogsCount, emojis = emojis,
favouritesCount = status.favouritesCount, reblogsCount = status.reblogsCount,
reblogged = status.reblogged, favouritesCount = status.favouritesCount,
favourited = status.favourited, reblogged = status.reblogged,
bookmarked = status.bookmarked, favourited = status.favourited,
sensitive = status.sensitive, bookmarked = status.bookmarked,
spoilerText = status.spoilerText!!, sensitive = status.sensitive,
visibility = status.visibility!!, spoilerText = status.spoilerText!!,
attachments = attachments, visibility = status.visibility!!,
mentions = mentions, attachments = attachments,
application = application, mentions = mentions,
pinned = false, application = application,
poll = poll, pinned = false,
card = null, poll = poll,
pleroma = pleroma card = null,
pleroma = pleroma
) )
} }
val status = if (reblog != null) { val status = if(reblog != null) {
Status( Status(
id = status.serverId, id = status.serverId,
url = null, // no url for reblogs url = null, // no url for reblogs
account = this.reblogAccount!!.toAccount(gson), account = this.reblogAccount!!.toAccount(gson),
inReplyToId = null, inReplyToId = null,
inReplyToAccountId = null, inReplyToAccountId = null,
reblog = reblog, reblog = reblog,
content = SpannedString(""), content = SpannedString(""),
createdAt = Date(status.createdAt), // lie but whatever? createdAt = Date(status.createdAt), // lie but whatever?
emojis = listOf(), emojis = listOf(),
reblogsCount = 0, reblogsCount = 0,
favouritesCount = 0, favouritesCount = 0,
reblogged = false, reblogged = false,
favourited = false, favourited = false,
bookmarked = false, bookmarked = false,
sensitive = false, sensitive = false,
spoilerText = "", spoilerText = "",
visibility = status.visibility!!, visibility = status.visibility!!,
attachments = ArrayList(), attachments = ArrayList(),
mentions = arrayOf(), mentions = arrayOf(),
application = null, application = null,
pinned = false, pinned = false,
poll = null, poll = null,
card = null, card = null,
pleroma = null pleroma = null
) )
} else { } else {
Status( Status(
id = status.serverId, id = status.serverId,
url = status.url, url = status.url,
account = account.toAccount(gson), account = account.toAccount(gson),
inReplyToId = status.inReplyToId, inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId, inReplyToAccountId = status.inReplyToAccountId,
reblog = null, reblog = null,
content = status.content?.parseAsHtml()?.trimTrailingWhitespace() ?: SpannedString(""), content = status.content?.parseAsHtml()?.trimTrailingWhitespace()
createdAt = Date(status.createdAt), ?: SpannedString(""),
emojis = emojis, createdAt = Date(status.createdAt),
reblogsCount = status.reblogsCount, emojis = emojis,
favouritesCount = status.favouritesCount, reblogsCount = status.reblogsCount,
reblogged = status.reblogged, favouritesCount = status.favouritesCount,
favourited = status.favourited, reblogged = status.reblogged,
bookmarked = status.bookmarked, favourited = status.favourited,
sensitive = status.sensitive, bookmarked = status.bookmarked,
spoilerText = status.spoilerText!!, sensitive = status.sensitive,
visibility = status.visibility!!, spoilerText = status.spoilerText!!,
attachments = attachments, visibility = status.visibility!!,
mentions = mentions, attachments = attachments,
application = application, mentions = mentions,
pinned = false, application = application,
poll = poll, pinned = false,
card = null, poll = poll,
pleroma = pleroma card = null,
pleroma = pleroma
) )
} }
return Either.Right(status) return Either.Right(status)
@ -295,98 +313,100 @@ private val emojisListTypeToken = object : TypeToken<List<Emoji>>() {}
fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity { fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
return TimelineAccountEntity( return TimelineAccountEntity(
serverId = id, serverId = id,
timelineUserId = accountId, timelineUserId = accountId,
localUsername = localUsername, localUsername = localUsername,
username = username, username = username,
displayName = displayName.orEmpty(), displayName = displayName.orEmpty(),
url = url, url = url,
avatar = avatar, avatar = avatar,
emojis = gson.toJson(emojis), emojis = gson.toJson(emojis),
bot = bot bot = bot
) )
} }
fun TimelineAccountEntity.toAccount(gson: Gson): Account { fun TimelineAccountEntity.toAccount(gson: Gson): Account {
return Account( return Account(
id = serverId, id = serverId,
localUsername = localUsername, localUsername = localUsername,
username = username, username = username,
displayName = displayName, displayName = displayName,
note = SpannedString(""), note = SpannedString(""),
url = url, url = url,
avatar = avatar, avatar = avatar,
header = "", header = "",
locked = false, locked = false,
followingCount = 0, followingCount = 0,
followersCount = 0, followersCount = 0,
statusesCount = 0, statusesCount = 0,
source = null, source = null,
bot = bot, bot = bot,
emojis = gson.fromJson(this.emojis, emojisListTypeToken.type), emojis = gson.fromJson(this.emojis, emojisListTypeToken.type),
fields = null, fields = null,
moved = null moved = null
) )
} }
fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
return TimelineStatusEntity( return TimelineStatusEntity(
serverId = this.id, serverId = this.id,
url = null, url = null,
timelineUserId = timelineUserId, timelineUserId = timelineUserId,
authorServerId = null, authorServerId = null,
inReplyToId = null, inReplyToId = null,
inReplyToAccountId = null, inReplyToAccountId = null,
content = null, content = null,
createdAt = 0L, createdAt = 0L,
emojis = null, emojis = null,
reblogsCount = 0, reblogsCount = 0,
favouritesCount = 0, favouritesCount = 0,
reblogged = false, reblogged = false,
favourited = false, favourited = false,
bookmarked = false, bookmarked = false,
sensitive = false, sensitive = false,
spoilerText = null, spoilerText = null,
visibility = null, visibility = null,
attachments = null, attachments = null,
mentions = null, mentions = null,
application = null, application = null,
reblogServerId = null, reblogServerId = null,
reblogAccountId = null, reblogAccountId = null,
poll = null, poll = null,
pleroma = null pleroma = null
) )
} }
fun Status.toEntity(timelineUserId: Long, fun Status.toEntity(
gson: Gson): TimelineStatusEntity { timelineUserId: Long,
gson: Gson
): TimelineStatusEntity {
val actionable = actionableStatus val actionable = actionableStatus
return TimelineStatusEntity( return TimelineStatusEntity(
serverId = this.id, serverId = this.id,
url = actionable.url!!, url = actionable.url!!,
timelineUserId = timelineUserId, timelineUserId = timelineUserId,
authorServerId = actionable.account.id, authorServerId = actionable.account.id,
inReplyToId = actionable.inReplyToId, inReplyToId = actionable.inReplyToId,
inReplyToAccountId = actionable.inReplyToAccountId, inReplyToAccountId = actionable.inReplyToAccountId,
content = actionable.content.toHtml(), content = actionable.content.toHtml(),
createdAt = actionable.createdAt.time, createdAt = actionable.createdAt.time,
emojis = actionable.emojis.let(gson::toJson), emojis = actionable.emojis.let(gson::toJson),
reblogsCount = actionable.reblogsCount, reblogsCount = actionable.reblogsCount,
favouritesCount = actionable.favouritesCount, favouritesCount = actionable.favouritesCount,
reblogged = actionable.reblogged, reblogged = actionable.reblogged,
favourited = actionable.favourited, favourited = actionable.favourited,
bookmarked = actionable.bookmarked, bookmarked = actionable.bookmarked,
sensitive = actionable.sensitive, sensitive = actionable.sensitive,
spoilerText = actionable.spoilerText, spoilerText = actionable.spoilerText,
visibility = actionable.visibility, visibility = actionable.visibility,
attachments = actionable.attachments.let(gson::toJson), attachments = actionable.attachments.let(gson::toJson),
mentions = actionable.mentions.let(gson::toJson), mentions = actionable.mentions.let(gson::toJson),
application = actionable.application.let(gson::toJson), application = actionable.application.let(gson::toJson),
reblogServerId = reblog?.id, reblogServerId = reblog?.id,
reblogAccountId = reblog?.let { this.account.id }, reblogAccountId = reblog?.let { this.account.id },
poll = actionable.poll.let(gson::toJson), poll = actionable.poll.let(gson::toJson),
pleroma = actionable.pleroma.let(gson::toJson) pleroma = actionable.pleroma.let(gson::toJson)
) )
} }