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(
|
||||
{
|
||||
|
|
|
@ -19,9 +19,9 @@ import android.text.SpannedString
|
|||
import android.widget.LinearLayout
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
|
@ -57,19 +57,13 @@ class BottomSheetActivityTest {
|
|||
private val emptyCallback = Single.just(SearchResult(emptyList(), emptyList(), emptyList()))
|
||||
private val testScheduler = TestScheduler()
|
||||
|
||||
private val account = Account(
|
||||
private val account = TimelineAccount(
|
||||
id = "1",
|
||||
localUsername = "admin",
|
||||
username = "admin",
|
||||
displayName = "Ad Min",
|
||||
note = SpannedString(""),
|
||||
url = "http://mastodon.foo.bar",
|
||||
avatar = "",
|
||||
header = "",
|
||||
locked = false,
|
||||
followersCount = 0,
|
||||
followingCount = 0,
|
||||
statusesCount = 0
|
||||
avatar = ""
|
||||
)
|
||||
private val accountSingle = Single.just(SearchResult(listOf(account), emptyList(), emptyList()))
|
||||
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.text.Spanned
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StatusComparisonTest {
|
||||
|
||||
@Test
|
||||
fun `two equal statuses - should be equal`() {
|
||||
assertEquals(createStatus(), createStatus())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `status with different id - should not be equal`() {
|
||||
assertNotEquals(createStatus(), createStatus(id = "987654321"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `status with different content - should not be equal`() {
|
||||
val content: String = """
|
||||
\u003cp\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.social/@ConnyDuck\" class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"\u003e@\u003cspan\u003eConnyDuck@mastodon.social\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e 123\u003c/p\u003e
|
||||
""".trimIndent()
|
||||
assertNotEquals(createStatus(), createStatus(content = content))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accounts with different notes in json - should be equal because notes are not relevant for timelines`() {
|
||||
assertEquals(createStatus(note = "Test"), createStatus(note = "Test 123456"))
|
||||
}
|
||||
|
||||
private val gson = GsonBuilder().registerTypeAdapter(
|
||||
Spanned::class.java, SpannedTypeAdapter()
|
||||
).create()
|
||||
|
||||
@Test
|
||||
fun `two equal status view data - should be equal`() {
|
||||
val viewdata1 = StatusViewData.Concrete(
|
||||
status = createStatus(),
|
||||
isExpanded = false,
|
||||
isShowingContent = false,
|
||||
isCollapsible = false,
|
||||
isCollapsed = false
|
||||
)
|
||||
val viewdata2 = StatusViewData.Concrete(
|
||||
status = createStatus(),
|
||||
isExpanded = false,
|
||||
isShowingContent = false,
|
||||
isCollapsible = false,
|
||||
isCollapsed = false
|
||||
)
|
||||
assertEquals(viewdata1, viewdata2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `status view data with different isExpanded - should not be equal`() {
|
||||
val viewdata1 = StatusViewData.Concrete(
|
||||
status = createStatus(),
|
||||
isExpanded = true,
|
||||
isShowingContent = false,
|
||||
isCollapsible = false,
|
||||
isCollapsed = false
|
||||
)
|
||||
val viewdata2 = StatusViewData.Concrete(
|
||||
status = createStatus(),
|
||||
isExpanded = false,
|
||||
isShowingContent = false,
|
||||
isCollapsible = false,
|
||||
isCollapsed = false
|
||||
)
|
||||
assertNotEquals(viewdata1, viewdata2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `status view data with different statuses- should not be equal`() {
|
||||
val viewdata1 = StatusViewData.Concrete(
|
||||
status = createStatus(content = "whatever"),
|
||||
isExpanded = true,
|
||||
isShowingContent = false,
|
||||
isCollapsible = false,
|
||||
isCollapsed = false
|
||||
)
|
||||
val viewdata2 = StatusViewData.Concrete(
|
||||
status = createStatus(),
|
||||
isExpanded = false,
|
||||
isShowingContent = false,
|
||||
isCollapsible = false,
|
||||
isCollapsed = false
|
||||
)
|
||||
assertNotEquals(viewdata1, viewdata2)
|
||||
}
|
||||
|
||||
private fun createStatus(
|
||||
id: String = "123456",
|
||||
content: String = """
|
||||
\u003cp\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.social/@ConnyDuck\" class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"\u003e@\u003cspan\u003eConnyDuck@mastodon.social\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e Hi\u003c/p\u003e
|
||||
""".trimIndent(),
|
||||
note: String = ""
|
||||
): Status {
|
||||
val statusJson = """
|
||||
{
|
||||
"id": "$id",
|
||||
"created_at": "2022-02-26T09:54:45.000Z",
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"visibility": "public",
|
||||
"language": null,
|
||||
"uri": "https://pixelfed.social/p/connyduck/403124983655733325",
|
||||
"url": "https://pixelfed.social/p/connyduck/403124983655733325",
|
||||
"replies_count": 3,
|
||||
"reblogs_count": 28,
|
||||
"favourites_count": 6,
|
||||
"edited_at": null,
|
||||
"favourited": true,
|
||||
"reblogged": false,
|
||||
"muted": false,
|
||||
"bookmarked": false,
|
||||
"content": "$content",
|
||||
"reblog": null,
|
||||
"account": {
|
||||
"id": "419352",
|
||||
"username": "connyduck",
|
||||
"acct": "connyduck@pixelfed.social",
|
||||
"display_name": "Conny Duck",
|
||||
"locked": false,
|
||||
"bot": false,
|
||||
"discoverable": false,
|
||||
"group": false,
|
||||
"created_at": "2018-08-14T00:00:00.000Z",
|
||||
"note": "$note",
|
||||
"url": "https://pixelfed.social/connyduck",
|
||||
"avatar": "https://files.mastodon.social/cache/accounts/avatars/000/419/352/original/31ce660c53962e0c.jpeg",
|
||||
"avatar_static": "https://files.mastodon.social/cache/accounts/avatars/000/419/352/original/31ce660c53962e0c.jpeg",
|
||||
"header": "https://mastodon.social/headers/original/missing.png",
|
||||
"header_static": "https://mastodon.social/headers/original/missing.png",
|
||||
"followers_count": 2,
|
||||
"following_count": 0,
|
||||
"statuses_count": 70,
|
||||
"last_status_at": "2022-03-07",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
"media_attachments": [
|
||||
{
|
||||
"id": "107863694400783337",
|
||||
"type": "image",
|
||||
"url": "https://files.mastodon.social/cache/media_attachments/files/107/863/694/400/783/337/original/71c5bad1756bbc8f.jpg",
|
||||
"preview_url": "https://files.mastodon.social/cache/media_attachments/files/107/863/694/400/783/337/small/71c5bad1756bbc8f.jpg",
|
||||
"remote_url": "https://pixelfed-prod.nyc3.cdn.digitaloceanspaces.com/public/m/_v2/1138/affc38a2b-1c5f41/JRKoMNoj6dKa/9mXs0Fetvj4KwRbKypt8C1PZNVd7d3dQqod4roLZ.jpg",
|
||||
"preview_remote_url": null,
|
||||
"text_url": null,
|
||||
"meta": {
|
||||
"original": {
|
||||
"width": 1371,
|
||||
"height": 1080,
|
||||
"size": "1371x1080",
|
||||
"aspect": 1.2694444444444444
|
||||
},
|
||||
"small": {
|
||||
"width": 451,
|
||||
"height": 355,
|
||||
"size": "451x355",
|
||||
"aspect": 1.2704225352112677
|
||||
}
|
||||
},
|
||||
"description": "Oilpainting of a kingfisher, photographed on my easel",
|
||||
"blurhash": "UUG91|?wxHV@WTkDs.V?xZa_I:WBNFR*WBRk"
|
||||
},
|
||||
{
|
||||
"id": "107863694727565058",
|
||||
"type": "image",
|
||||
"url": "https://files.mastodon.social/cache/media_attachments/files/107/863/694/727/565/058/original/68daef05be7ac6b6.jpg",
|
||||
"preview_url": "https://files.mastodon.social/cache/media_attachments/files/107/863/694/727/565/058/small/68daef05be7ac6b6.jpg",
|
||||
"remote_url": "https://pixelfed-prod.nyc3.cdn.digitaloceanspaces.com/public/m/_v2/1138/affc38a2b-1c5f41/nBVJUnrEIjfO/M6i8GSP44Iv230KWXnMpvVobOqASXY3EkImyxySS.jpg",
|
||||
"preview_remote_url": null,
|
||||
"text_url": null,
|
||||
"meta": {
|
||||
"original": {
|
||||
"width": 1087,
|
||||
"height": 1080,
|
||||
"size": "1087x1080",
|
||||
"aspect": 1.0064814814814815
|
||||
},
|
||||
"small": {
|
||||
"width": 401,
|
||||
"height": 398,
|
||||
"size": "401x398",
|
||||
"aspect": 1.0075376884422111
|
||||
}
|
||||
},
|
||||
"description": "Oilpainting of a kingfisher",
|
||||
"blurhash": "U89u4pPJ4:SoJ6NNnkoxoBtSx0Von-RiNgt8"
|
||||
}
|
||||
],
|
||||
"mentions": [],
|
||||
"tags": [],
|
||||
"emojis": [],
|
||||
"card": null,
|
||||
"poll": null
|
||||
}
|
||||
""".trimIndent()
|
||||
return gson.fromJson(statusJson, Status::class.java)
|
||||
}
|
||||
}
|
|
@ -3,8 +3,8 @@ package com.keylesspalace.tusky.components.timeline
|
|||
import android.text.SpannedString
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import java.util.ArrayList
|
||||
import java.util.Date
|
||||
|
@ -14,15 +14,13 @@ private val fixedDate = Date(1638889052000)
|
|||
fun mockStatus(id: String = "100") = Status(
|
||||
id = id,
|
||||
url = "https://mastodon.example/@ConnyDuck/$id",
|
||||
account = Account(
|
||||
account = TimelineAccount(
|
||||
id = "1",
|
||||
localUsername = "connyduck",
|
||||
username = "connyduck@mastodon.example",
|
||||
displayName = "Conny Duck",
|
||||
note = SpannedString(""),
|
||||
url = "https://mastodon.example/@ConnyDuck",
|
||||
avatar = "https://mastodon.example/system/accounts/avatars/000/150/486/original/ab27d7ddd18a10ea.jpg",
|
||||
header = "https://mastodon.example/system/accounts/header/000/106/476/original/e590545d7eb4da39.jpg"
|
||||
avatar = "https://mastodon.example/system/accounts/avatars/000/150/486/original/ab27d7ddd18a10ea.jpg"
|
||||
),
|
||||
inReplyToId = null,
|
||||
inReplyToAccountId = null,
|
||||
|
|
Loading…
Reference in a new issue