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