Fix status diffing and improve timeline performance (#2386)

* fix status & account diffing

* introduce TimelineAccount

* use TimelineAccount where possible

* improve tests

* improve ConversationEntity equals/hashcode

* fix mistake in ConversationEntity

* improve StatusViewData comparison

* improve tests

* fix typo in comment
This commit is contained in:
Konrad Pozniak 2022-03-15 21:34:57 +01:00 committed by GitHub
commit e05fdc6d7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 463 additions and 147 deletions

View file

@ -45,37 +45,57 @@ data class Account(
localUsername
} else displayName
override fun hashCode(): Int {
return id.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other !is Account) {
return false
}
return other.id == this.id
}
fun deepEquals(other: Account): Boolean {
return id == other.id &&
localUsername == other.localUsername &&
displayName == other.displayName &&
note == other.note &&
url == other.url &&
avatar == other.avatar &&
header == other.header &&
locked == other.locked &&
followersCount == other.followersCount &&
followingCount == other.followingCount &&
statusesCount == other.statusesCount &&
source == other.source &&
bot == other.bot &&
emojis == other.emojis &&
fields == other.fields &&
moved == other.moved
}
fun isRemote(): Boolean = this.username != this.localUsername
/**
* overriding equals & hashcode because Spanned does not always compare correctly otherwise
*/
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Account
if (id != other.id) return false
if (localUsername != other.localUsername) return false
if (username != other.username) return false
if (displayName != other.displayName) return false
if (note.toString() != other.note.toString()) return false
if (url != other.url) return false
if (avatar != other.avatar) return false
if (header != other.header) return false
if (locked != other.locked) return false
if (followersCount != other.followersCount) return false
if (followingCount != other.followingCount) return false
if (statusesCount != other.statusesCount) return false
if (source != other.source) return false
if (bot != other.bot) return false
if (emojis != other.emojis) return false
if (fields != other.fields) return false
if (moved != other.moved) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + localUsername.hashCode()
result = 31 * result + username.hashCode()
result = 31 * result + (displayName?.hashCode() ?: 0)
result = 31 * result + note.toString().hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + avatar.hashCode()
result = 31 * result + header.hashCode()
result = 31 * result + locked.hashCode()
result = 31 * result + followersCount
result = 31 * result + followingCount
result = 31 * result + statusesCount
result = 31 * result + (source?.hashCode() ?: 0)
result = 31 * result + bot.hashCode()
result = 31 * result + (emojis?.hashCode() ?: 0)
result = 31 * result + (fields?.hashCode() ?: 0)
result = 31 * result + (moved?.hashCode() ?: 0)
return result
}
}
data class AccountSource(

View file

@ -19,7 +19,7 @@ import com.google.gson.annotations.SerializedName
data class Conversation(
val id: String,
val accounts: List<Account>,
val accounts: List<TimelineAccount>,
@SerializedName("last_status") val lastStatus: Status?, // should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038
val unread: Boolean
)

View file

@ -24,7 +24,7 @@ import com.google.gson.annotations.JsonAdapter
data class Notification(
val type: Type,
val id: String,
val account: Account,
val account: TimelineAccount,
val status: Status?
) {

View file

@ -16,7 +16,7 @@
package com.keylesspalace.tusky.entity
data class SearchResult(
val accounts: List<Account>,
val accounts: List<TimelineAccount>,
val statuses: List<Status>,
val hashtags: List<HashTag>
)

View file

@ -25,7 +25,7 @@ import java.util.Date
data class Status(
val id: String,
val url: String?, // not present if it's reblog
val account: Account,
val account: TimelineAccount,
@SerializedName("in_reply_to_id") var inReplyToId: String?,
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
val reblog: Status?,
@ -149,6 +149,71 @@ data class Status(
return builder.toString()
}
/**
* overriding equals & hashcode because Spanned does not always compare correctly otherwise
*/
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Status
if (id != other.id) return false
if (url != other.url) return false
if (account != other.account) return false
if (inReplyToId != other.inReplyToId) return false
if (inReplyToAccountId != other.inReplyToAccountId) return false
if (reblog != other.reblog) return false
if (content.toString() != other.content.toString()) return false
if (createdAt != other.createdAt) return false
if (emojis != other.emojis) return false
if (reblogsCount != other.reblogsCount) return false
if (favouritesCount != other.favouritesCount) return false
if (reblogged != other.reblogged) return false
if (favourited != other.favourited) return false
if (bookmarked != other.bookmarked) return false
if (sensitive != other.sensitive) return false
if (spoilerText != other.spoilerText) return false
if (visibility != other.visibility) return false
if (attachments != other.attachments) return false
if (mentions != other.mentions) return false
if (tags != other.tags) return false
if (application != other.application) return false
if (pinned != other.pinned) return false
if (muted != other.muted) return false
if (poll != other.poll) return false
if (card != other.card) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + (url?.hashCode() ?: 0)
result = 31 * result + account.hashCode()
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
result = 31 * result + (reblog?.hashCode() ?: 0)
result = 31 * result + content.toString().hashCode()
result = 31 * result + createdAt.hashCode()
result = 31 * result + emojis.hashCode()
result = 31 * result + reblogsCount
result = 31 * result + favouritesCount
result = 31 * result + reblogged.hashCode()
result = 31 * result + favourited.hashCode()
result = 31 * result + bookmarked.hashCode()
result = 31 * result + sensitive.hashCode()
result = 31 * result + spoilerText.hashCode()
result = 31 * result + visibility.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + mentions.hashCode()
result = 31 * result + (tags?.hashCode() ?: 0)
result = 31 * result + (application?.hashCode() ?: 0)
result = 31 * result + (pinned?.hashCode() ?: 0)
result = 31 * result + (muted?.hashCode() ?: 0)
result = 31 * result + (poll?.hashCode() ?: 0)
result = 31 * result + (card?.hashCode() ?: 0)
return result
}
data class Mention(
val id: String,
val url: String,

View file

@ -0,0 +1,39 @@
/* Copyright 2017 Andrew Dawson
*
* 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.entity
import com.google.gson.annotations.SerializedName
/**
* Same as [Account], but only with the attributes required in timelines.
* Prefer this class over [Account] because it uses way less memory & deserializes faster from json.
*/
data class TimelineAccount(
val id: String,
@SerializedName("username") val localUsername: String,
@SerializedName("acct") val username: String,
@SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract
val url: String,
val avatar: String,
val bot: Boolean = false,
val emojis: List<Emoji>? = emptyList(), // nullable for backward compatibility
) {
val name: String
get() = if (displayName.isNullOrEmpty()) {
localUsername
} else displayName
}