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:
parent
6e4a9fb0e6
commit
e05fdc6d7b
27 changed files with 463 additions and 147 deletions
|
@ -34,7 +34,7 @@ import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
|
|||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
|
@ -49,7 +49,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
private typealias AccountInfo = Pair<Account, Boolean>
|
||||
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
|
||||
|
||||
class AccountsInListFragment : DialogFragment(), Injectable {
|
||||
|
||||
|
@ -168,21 +168,21 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
viewModel.deleteAccountFromList(listId, accountId)
|
||||
}
|
||||
|
||||
private fun onAddToList(account: Account) {
|
||||
private fun onAddToList(account: TimelineAccount) {
|
||||
viewModel.addAccountToList(listId, account)
|
||||
}
|
||||
|
||||
private object AccountDiffer : DiffUtil.ItemCallback<Account>() {
|
||||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean {
|
||||
return oldItem == newItem
|
||||
private object AccountDiffer : DiffUtil.ItemCallback<TimelineAccount>() {
|
||||
override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean {
|
||||
return oldItem.deepEquals(newItem)
|
||||
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
inner class Adapter : ListAdapter<Account, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
|
||||
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
|
@ -209,12 +209,11 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
|
||||
private object SearchDiffer : DiffUtil.ItemCallback<AccountInfo>() {
|
||||
override fun areItemsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||
return oldItem == newItem
|
||||
return oldItem.first.id == newItem.first.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||
return oldItem.second == newItem.second &&
|
||||
oldItem.first.deepEquals(newItem.first)
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import android.view.LayoutInflater
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.removeDuplicates
|
||||
|
||||
|
@ -28,7 +28,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
|||
protected val animateAvatar: Boolean,
|
||||
protected val animateEmojis: Boolean
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder?>() {
|
||||
var accountList = mutableListOf<Account>()
|
||||
var accountList = mutableListOf<TimelineAccount>()
|
||||
private var bottomLoading: Boolean = false
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
|
@ -73,12 +73,12 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
|||
}
|
||||
}
|
||||
|
||||
fun update(newAccounts: List<Account>) {
|
||||
fun update(newAccounts: List<TimelineAccount>) {
|
||||
accountList = removeDuplicates(newAccounts)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun addItems(newAccounts: List<Account>) {
|
||||
fun addItems(newAccounts: List<TimelineAccount>) {
|
||||
val end = accountList.size
|
||||
val last = accountList[end - 1]
|
||||
if (newAccounts.none { it.id == last.id }) {
|
||||
|
@ -100,7 +100,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
|||
}
|
||||
}
|
||||
|
||||
fun removeItem(position: Int): Account? {
|
||||
fun removeItem(position: Int): TimelineAccount? {
|
||||
if (position < 0 || position >= accountList.size) {
|
||||
return null
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
|||
return account
|
||||
}
|
||||
|
||||
fun addItem(account: Account, position: Int) {
|
||||
fun addItem(account: TimelineAccount, position: Int) {
|
||||
if (position < 0 || position > accountList.size) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceManager;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
|
@ -33,7 +33,7 @@ public class AccountViewHolder extends RecyclerView.ViewHolder {
|
|||
showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true);
|
||||
}
|
||||
|
||||
public void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) {
|
||||
public void setupWithAccount(TimelineAccount account, boolean animateAvatar, boolean animateEmojis) {
|
||||
accountId = account.getId();
|
||||
String format = username.getContext().getString(R.string.status_username_format);
|
||||
String formattedUsername = String.format(format, account.getUsername());
|
||||
|
|
|
@ -22,7 +22,7 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
|
@ -55,7 +55,7 @@ class BlocksAdapter(
|
|||
private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock)
|
||||
private var id: String? = null
|
||||
|
||||
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||
fun setupWithAccount(account: TimelineAccount, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||
id = account.id
|
||||
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||
displayName.text = emojifiedName
|
||||
|
|
|
@ -22,7 +22,7 @@ import android.text.style.StyleSpan
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
|
@ -34,7 +34,7 @@ class FollowRequestViewHolder(
|
|||
private val showHeader: Boolean
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||
fun setupWithAccount(account: TimelineAccount, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||
val wrappedName = account.name.unicodeWrap()
|
||||
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
|
||||
binding.displayNameTextView.text = emojifiedName
|
||||
|
|
|
@ -9,7 +9,7 @@ import android.widget.TextView
|
|||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
|
@ -69,7 +69,7 @@ class MutesAdapter(
|
|||
private var notifications = false
|
||||
|
||||
fun setupWithAccount(
|
||||
account: Account,
|
||||
account: TimelineAccount,
|
||||
mutingNotifications: Boolean?,
|
||||
animateAvatar: Boolean,
|
||||
animateEmojis: Boolean
|
||||
|
|
|
@ -40,10 +40,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.bumptech.glide.Glide;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
|
@ -335,7 +335,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
}
|
||||
|
||||
void setMessage(Account account) {
|
||||
void setMessage(TimelineAccount account) {
|
||||
Context context = message.getContext();
|
||||
|
||||
String format = context.getString(R.string.notification_follow_format);
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
package com.keylesspalace.tusky.components.compose;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -28,9 +27,9 @@ import android.widget.TextView;
|
|||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.HashTag;
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
|
||||
|
@ -144,7 +143,7 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
|
|||
|
||||
AccountResult accountResult = ((AccountResult) getItem(position));
|
||||
if (accountResult != null) {
|
||||
Account account = accountResult.account;
|
||||
TimelineAccount account = accountResult.account;
|
||||
String formattedUsername = context.getString(
|
||||
R.string.status_username_format,
|
||||
account.getUsername()
|
||||
|
@ -268,9 +267,9 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
|
|||
}
|
||||
|
||||
public final static class AccountResult extends AutocompleteResult {
|
||||
private final Account account;
|
||||
private final TimelineAccount account;
|
||||
|
||||
public AccountResult(Account account) {
|
||||
public AccountResult(TimelineAccount account) {
|
||||
this.account = account;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,18 +16,17 @@
|
|||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.TypeConverters
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Conversation
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
import java.util.Date
|
||||
|
||||
|
@ -48,17 +47,15 @@ data class ConversationAccountEntity(
|
|||
val avatar: String,
|
||||
val emojis: List<Emoji>
|
||||
) {
|
||||
fun toAccount(): Account {
|
||||
return Account(
|
||||
fun toAccount(): TimelineAccount {
|
||||
return TimelineAccount(
|
||||
id = id,
|
||||
username = username,
|
||||
displayName = displayName,
|
||||
url = "",
|
||||
avatar = avatar,
|
||||
emojis = emojis,
|
||||
url = "",
|
||||
localUsername = "",
|
||||
note = SpannedString(""),
|
||||
header = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +97,7 @@ data class ConversationStatusEntity(
|
|||
if (inReplyToId != other.inReplyToId) return false
|
||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||
if (account != other.account) return false
|
||||
if (content.toString() != other.content.toString()) return false // TODO find a better method to compare two spanned strings
|
||||
if (content.toString() != other.content.toString()) return false
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (emojis != other.emojis) return false
|
||||
if (favouritesCount != other.favouritesCount) return false
|
||||
|
@ -126,7 +123,7 @@ data class ConversationStatusEntity(
|
|||
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
||||
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
||||
result = 31 * result + account.hashCode()
|
||||
result = 31 * result + content.hashCode()
|
||||
result = 31 * result + content.toString().hashCode()
|
||||
result = 31 * result + createdAt.hashCode()
|
||||
result = 31 * result + emojis.hashCode()
|
||||
result = 31 * result + favouritesCount
|
||||
|
@ -176,7 +173,7 @@ data class ConversationStatusEntity(
|
|||
}
|
||||
}
|
||||
|
||||
fun Account.toEntity() =
|
||||
fun TimelineAccount.toEntity() =
|
||||
ConversationAccountEntity(
|
||||
id = id,
|
||||
username = username,
|
||||
|
|
|
@ -21,11 +21,11 @@ import androidx.paging.PagingDataAdapter
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.AccountViewHolder
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
|
||||
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) :
|
||||
PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) {
|
||||
PagingDataAdapter<TimelineAccount, AccountViewHolder>(ACCOUNT_COMPARATOR) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
|
@ -44,11 +44,11 @@ class SearchAccountsAdapter(private val linkListener: LinkListener, private val
|
|||
|
||||
companion object {
|
||||
|
||||
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<Account>() {
|
||||
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean =
|
||||
oldItem.deepEquals(newItem)
|
||||
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<TimelineAccount>() {
|
||||
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean =
|
||||
oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean =
|
||||
override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,12 +19,12 @@ import androidx.paging.PagingData
|
|||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class SearchAccountsFragment : SearchFragment<Account>() {
|
||||
override fun createAdapter(): PagingDataAdapter<Account, *> {
|
||||
class SearchAccountsFragment : SearchFragment<TimelineAccount>() {
|
||||
override fun createAdapter(): PagingDataAdapter<TimelineAccount, *> {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
||||
|
||||
return SearchAccountsAdapter(
|
||||
|
@ -34,7 +34,7 @@ class SearchAccountsFragment : SearchFragment<Account>() {
|
|||
)
|
||||
}
|
||||
|
||||
override val data: Flow<PagingData<Account>>
|
||||
override val data: Flow<PagingData<TimelineAccount>>
|
||||
get() = viewModel.accountsFlow
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -114,7 +114,7 @@ class TimelinePagingAdapter(
|
|||
oldItem: StatusViewData,
|
||||
newItem: StatusViewData
|
||||
): Boolean {
|
||||
return oldItem.viewDataId == newItem.viewDataId
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
|
@ -128,7 +128,7 @@ class TimelinePagingAdapter(
|
|||
oldItem: StatusViewData,
|
||||
newItem: StatusViewData
|
||||
): Any? {
|
||||
return if (oldItem === newItem) {
|
||||
return if (oldItem == newItem) {
|
||||
// If items are equal - update timestamp only
|
||||
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
||||
} else // If items are different - update the whole view holder
|
||||
|
|
|
@ -23,12 +23,12 @@ import com.google.gson.reflect.TypeToken
|
|||
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
||||
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.HashTag
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
@ -44,7 +44,7 @@ private val emojisListType = object : TypeToken<List<Emoji>>() {}.type
|
|||
private val mentionListType = object : TypeToken<List<Status.Mention>>() {}.type
|
||||
private val tagListType = object : TypeToken<List<HashTag>>() {}.type
|
||||
|
||||
fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
||||
fun TimelineAccount.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
||||
return TimelineAccountEntity(
|
||||
serverId = id,
|
||||
timelineUserId = accountId,
|
||||
|
@ -58,25 +58,16 @@ fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
|||
)
|
||||
}
|
||||
|
||||
fun TimelineAccountEntity.toAccount(gson: Gson): Account {
|
||||
return Account(
|
||||
fun TimelineAccountEntity.toAccount(gson: Gson): TimelineAccount {
|
||||
return TimelineAccount(
|
||||
id = serverId,
|
||||
localUsername = localUsername,
|
||||
username = username,
|
||||
displayName = displayName,
|
||||
note = SpannedString(""),
|
||||
url = url,
|
||||
avatar = avatar,
|
||||
header = "",
|
||||
locked = false,
|
||||
followingCount = 0,
|
||||
followersCount = 0,
|
||||
statusesCount = 0,
|
||||
source = null,
|
||||
bot = bot,
|
||||
emojis = gson.fromJson(emojis, emojisListType),
|
||||
fields = null,
|
||||
moved = null
|
||||
emojis = gson.fromJson(emojis, emojisListType)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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?
|
||||
) {
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -42,8 +42,8 @@ import com.keylesspalace.tusky.components.account.AccountActivity
|
|||
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
|
@ -255,7 +255,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
|
|||
followRequestsAdapter.removeItem(position)
|
||||
}
|
||||
|
||||
private fun getFetchCallByListType(fromId: String?): Single<Response<List<Account>>> {
|
||||
private fun getFetchCallByListType(fromId: String?): Single<Response<List<TimelineAccount>>> {
|
||||
return when (type) {
|
||||
Type.FOLLOWS -> {
|
||||
val accountId = requireId(type, id)
|
||||
|
@ -313,7 +313,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
|
|||
)
|
||||
}
|
||||
|
||||
private fun onFetchAccountsSuccess(accounts: List<Account>, linkHeader: String?) {
|
||||
private fun onFetchAccountsSuccess(accounts: List<TimelineAccount>, linkHeader: String?) {
|
||||
adapter.setBottomLoading(false)
|
||||
|
||||
val links = HttpHeaderLink.parse(linkHeader)
|
||||
|
|
|
@ -37,6 +37,7 @@ import com.keylesspalace.tusky.entity.ScheduledStatus
|
|||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.StatusContext
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import okhttp3.MultipartBody
|
||||
|
@ -178,13 +179,13 @@ interface MastodonApi {
|
|||
fun statusRebloggedBy(
|
||||
@Path("id") statusId: String,
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
): Single<Response<List<TimelineAccount>>>
|
||||
|
||||
@GET("api/v1/statuses/{id}/favourited_by")
|
||||
fun statusFavouritedBy(
|
||||
@Path("id") statusId: String,
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
): Single<Response<List<TimelineAccount>>>
|
||||
|
||||
@DELETE("api/v1/statuses/{id}")
|
||||
fun deleteStatus(
|
||||
|
@ -286,7 +287,7 @@ interface MastodonApi {
|
|||
@Query("resolve") resolve: Boolean? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("following") following: Boolean? = null
|
||||
): Single<List<Account>>
|
||||
): Single<List<TimelineAccount>>
|
||||
|
||||
@GET("api/v1/accounts/{id}")
|
||||
fun account(
|
||||
|
@ -317,13 +318,13 @@ interface MastodonApi {
|
|||
fun accountFollowers(
|
||||
@Path("id") accountId: String,
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
): Single<Response<List<TimelineAccount>>>
|
||||
|
||||
@GET("api/v1/accounts/{id}/following")
|
||||
fun accountFollowing(
|
||||
@Path("id") accountId: String,
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
): Single<Response<List<TimelineAccount>>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/accounts/{id}/follow")
|
||||
|
@ -384,12 +385,12 @@ interface MastodonApi {
|
|||
@GET("api/v1/blocks")
|
||||
fun blocks(
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
): Single<Response<List<TimelineAccount>>>
|
||||
|
||||
@GET("api/v1/mutes")
|
||||
fun mutes(
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
): Single<Response<List<TimelineAccount>>>
|
||||
|
||||
@GET("api/v1/domain_blocks")
|
||||
fun domainBlocks(
|
||||
|
@ -426,7 +427,7 @@ interface MastodonApi {
|
|||
@GET("api/v1/follow_requests")
|
||||
fun followRequests(
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
): Single<Response<List<TimelineAccount>>>
|
||||
|
||||
@POST("api/v1/follow_requests/{id}/authorize")
|
||||
fun authorizeFollowRequest(
|
||||
|
@ -481,7 +482,7 @@ interface MastodonApi {
|
|||
fun getAccountsInList(
|
||||
@Path("listId") listId: String,
|
||||
@Query("limit") limit: Int
|
||||
): Single<List<Account>>
|
||||
): Single<List<TimelineAccount>>
|
||||
|
||||
@FormUrlEncoded
|
||||
// @DELETE doesn't support fields
|
||||
|
|
|
@ -19,6 +19,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -44,11 +45,11 @@ public abstract class NotificationViewData {
|
|||
public static final class Concrete extends NotificationViewData {
|
||||
private final Notification.Type type;
|
||||
private final String id;
|
||||
private final Account account;
|
||||
private final TimelineAccount account;
|
||||
@Nullable
|
||||
private final StatusViewData.Concrete statusViewData;
|
||||
|
||||
public Concrete(Notification.Type type, String id, Account account,
|
||||
public Concrete(Notification.Type type, String id, TimelineAccount account,
|
||||
@Nullable StatusViewData.Concrete statusViewData) {
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
|
@ -64,7 +65,7 @@ public abstract class NotificationViewData {
|
|||
return id;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
public TimelineAccount getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,12 +22,11 @@ import com.keylesspalace.tusky.entity.Status
|
|||
/**
|
||||
* Created by charlag on 11/07/2017.
|
||||
*
|
||||
*
|
||||
* Class to represent data required to display either a notification or a placeholder.
|
||||
* It is either a [StatusViewData.Concrete] or a [StatusViewData.Placeholder].
|
||||
*/
|
||||
sealed class StatusViewData private constructor() {
|
||||
abstract val viewDataId: Long
|
||||
sealed class StatusViewData {
|
||||
abstract val id: String
|
||||
|
||||
data class Concrete(
|
||||
val status: Status,
|
||||
|
@ -49,8 +48,8 @@ sealed class StatusViewData private constructor() {
|
|||
/** Whether the status meets the requirement to be collapse */
|
||||
val isCollapsed: Boolean,
|
||||
) : StatusViewData() {
|
||||
override val viewDataId: Long
|
||||
get() = status.id.hashCode().toLong()
|
||||
override val id: String
|
||||
get() = status.id
|
||||
|
||||
val content: Spanned
|
||||
val spoilerText: String
|
||||
|
@ -116,9 +115,6 @@ sealed class StatusViewData private constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
val id: String
|
||||
get() = status.id
|
||||
|
||||
/** Helper for Java */
|
||||
fun copyWithStatus(status: Status): Concrete {
|
||||
return copy(status = status)
|
||||
|
@ -140,10 +136,10 @@ sealed class StatusViewData private constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
data class Placeholder(val id: String, val isLoading: Boolean) : StatusViewData() {
|
||||
override val viewDataId: Long
|
||||
get() = id.hashCode().toLong()
|
||||
}
|
||||
data class Placeholder(
|
||||
override val id: String,
|
||||
val isLoading: Boolean
|
||||
) : StatusViewData()
|
||||
|
||||
fun asStatusOrNull() = this as? Concrete
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
package com.keylesspalace.tusky.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.Either.Left
|
||||
|
@ -28,7 +28,7 @@ import io.reactivex.rxjava3.core.Observable
|
|||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import javax.inject.Inject
|
||||
|
||||
data class State(val accounts: Either<Throwable, List<Account>>, val searchResult: List<Account>?)
|
||||
data class State(val accounts: Either<Throwable, List<TimelineAccount>>, val searchResult: List<TimelineAccount>?)
|
||||
|
||||
class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() {
|
||||
|
||||
|
@ -49,7 +49,7 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi)
|
|||
}
|
||||
}
|
||||
|
||||
fun addAccountToList(listId: String, account: Account) {
|
||||
fun addAccountToList(listId: String, account: TimelineAccount) {
|
||||
api.addCountToList(listId, listOf(account.id))
|
||||
.subscribe(
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue