Animated emoji support (#2064)

* Animated emoji support

* Try to query preference only once

* Revert to using SpannableStringBuilder
This commit is contained in:
Alibek Omarov 2021-02-06 10:14:51 +03:00 committed by GitHub
parent c685192d49
commit 9580870445
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 225 additions and 120 deletions

View File

@ -78,7 +78,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private val viewModel: AccountViewModel by viewModels { viewModelFactory } private val viewModel: AccountViewModel by viewModels { viewModelFactory }
private val accountFieldAdapter = AccountFieldAdapter(this) private lateinit var accountFieldAdapter : AccountFieldAdapter
private var followState: FollowState = FollowState.NOT_FOLLOWING private var followState: FollowState = FollowState.NOT_FOLLOWING
private var blocking: Boolean = false private var blocking: Boolean = false
@ -89,6 +89,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private var loadedAccount: Account? = null private var loadedAccount: Account? = null
private var animateAvatar: Boolean = false private var animateAvatar: Boolean = false
private var animateEmojis: Boolean = false
// fields for scroll animation // fields for scroll animation
private var hideFab: Boolean = false private var hideFab: Boolean = false
@ -124,6 +125,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false) animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false)
animateEmojis = sharedPrefs.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
hideFab = sharedPrefs.getBoolean("fabHide", false) hideFab = sharedPrefs.getBoolean("fabHide", false)
setupToolbar() setupToolbar()
@ -162,6 +164,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountFollowsYouTextView.hide() accountFollowsYouTextView.hide()
// setup the RecyclerView for the account fields // setup the RecyclerView for the account fields
accountFieldAdapter = AccountFieldAdapter(this, animateEmojis)
accountFieldList.isNestedScrollingEnabled = false accountFieldList.isNestedScrollingEnabled = false
accountFieldList.layoutManager = LinearLayoutManager(this) accountFieldList.layoutManager = LinearLayoutManager(this)
accountFieldList.adapter = accountFieldAdapter accountFieldList.adapter = accountFieldAdapter
@ -375,9 +378,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val usernameFormatted = getString(R.string.status_username_format, account.username) val usernameFormatted = getString(R.string.status_username_format, account.username)
accountUsernameTextView.text = usernameFormatted accountUsernameTextView.text = usernameFormatted
accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView) accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView, animateEmojis)
val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView) val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView, animateEmojis)
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this) LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this)
// accountFieldAdapter.fields = account.fields ?: emptyList() // accountFieldAdapter.fields = account.fields ?: emptyList()
@ -437,7 +440,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun updateToolbar() { private fun updateToolbar() {
loadedAccount?.let { account -> loadedAccount?.let { account ->
val emojifiedName = account.name.emojify(account.emojis, accountToolbar) val emojifiedName = account.name.emojify(account.emojis, accountToolbar, animateEmojis)
try { try {
supportActionBar?.title = EmojiCompat.get().process(emojifiedName) supportActionBar?.title = EmojiCompat.get().process(emojifiedName)

View File

@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
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.Account
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
import com.keylesspalace.tusky.viewmodel.State import com.keylesspalace.tusky.viewmodel.State
@ -71,7 +72,9 @@ class AccountsInListFragment : DialogFragment(), Injectable {
private val searchAdapter = SearchAdapter() private val searchAdapter = SearchAdapter()
private val radius by lazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) } private val radius by lazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) }
private val animateAvatar by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("animateGifAvatars", false) } private val pm by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) }
private val animateAvatar by lazy { pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) }
private val animateEmojis by lazy { pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -209,7 +212,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
} }
fun bind(account: Account) { fun bind(account: Account) {
displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView) displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis)
usernameTextView.text = account.username usernameTextView.text = account.username
loadAvatar(account.avatar, avatar, radius, animateAvatar) loadAvatar(account.avatar, avatar, radius, animateAvatar)
} }
@ -252,7 +255,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
override val containerView = itemView override val containerView = itemView
fun bind(account: Account, inAList: Boolean) { fun bind(account: Account, inAList: Boolean) {
displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView) displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis)
usernameTextView.text = account.username usernameTextView.text = account.username
loadAvatar(account.avatar, avatar, radius, animateAvatar) loadAvatar(account.avatar, avatar, radius, animateAvatar)

View File

@ -723,8 +723,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
private fun updateProfiles() { private fun updateProfiles() {
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc -> val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header)) val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))
ProfileDrawerItem().apply { ProfileDrawerItem().apply {
isSelected = acc.isActive isSelected = acc.isActive

View File

@ -33,10 +33,14 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
List<Account> accountList; List<Account> accountList;
AccountActionListener accountActionListener; AccountActionListener accountActionListener;
private boolean bottomLoading; private boolean bottomLoading;
protected final boolean animateEmojis;
protected final boolean animateAvatar;
AccountAdapter(AccountActionListener accountActionListener) { AccountAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
this.accountList = new ArrayList<>(); this.accountList = new ArrayList<>();
this.accountActionListener = accountActionListener; this.accountActionListener = accountActionListener;
this.animateAvatar = animateAvatar;
this.animateEmojis = animateEmojis;
bottomLoading = false; bottomLoading = false;
} }

View File

@ -29,7 +29,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.item_account_field.view.* import kotlinx.android.synthetic.main.item_account_field.view.*
class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView.Adapter<AccountFieldAdapter.ViewHolder>() { class AccountFieldAdapter(private val linkListener: LinkListener, private val animateEmojis: Boolean) : RecyclerView.Adapter<AccountFieldAdapter.ViewHolder>() {
var emojis: List<Emoji> = emptyList() var emojis: List<Emoji> = emptyList()
var fields: List<Either<IdentityProof, Field>> = emptyList() var fields: List<Either<IdentityProof, Field>> = emptyList()
@ -55,10 +55,10 @@ class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
} else { } else {
val field = proofOrField.asRight() val field = proofOrField.asRight()
val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView) val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView, animateEmojis)
viewHolder.nameTextView.text = emojifiedName viewHolder.nameTextView.text = emojifiedName
val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView) val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView, animateEmojis)
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener) LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener)
if(field.verifiedAt != null) { if(field.verifiedAt != null) {

View File

@ -23,6 +23,7 @@ import android.widget.ArrayAdapter
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.item_autocomplete_account.view.* import kotlinx.android.synthetic.main.item_autocomplete_account.view.*
@ -41,12 +42,14 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(co
val username = view.username val username = view.username
val displayName = view.display_name val displayName = view.display_name
val avatar = view.avatar val avatar = view.avatar
val pm = PreferenceManager.getDefaultSharedPreferences(avatar.context)
val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
username.text = account.fullName username.text = account.fullName
displayName.text = account.displayName.emojify(account.emojis, displayName) displayName.text = account.displayName.emojify(account.emojis, displayName, animateEmojis)
val avatarRadius = avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) val avatarRadius = avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
val animateAvatar = PreferenceManager.getDefaultSharedPreferences(avatar.context) val animateAvatar = pm.getBoolean("animateGifAvatars", false)
.getBoolean("animateGifAvatars", false)
loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar) loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar)

View File

@ -22,7 +22,6 @@ public class AccountViewHolder extends RecyclerView.ViewHolder {
private ImageView avatarInset; private ImageView avatarInset;
private String accountId; private String accountId;
private boolean showBotOverlay; private boolean showBotOverlay;
private boolean animateAvatar;
public AccountViewHolder(View itemView) { public AccountViewHolder(View itemView) {
super(itemView); super(itemView);
@ -32,15 +31,14 @@ public class AccountViewHolder extends RecyclerView.ViewHolder {
avatarInset = itemView.findViewById(R.id.account_avatar_inset); avatarInset = itemView.findViewById(R.id.account_avatar_inset);
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()); SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(itemView.getContext());
showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true); showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true);
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false);
} }
public void setupWithAccount(Account account) { public void setupWithAccount(Account 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());
username.setText(formattedUsername); username.setText(formattedUsername);
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
displayName.setText(emojifiedName); displayName.setText(emojifiedName);
int avatarRadius = avatar.getContext().getResources() int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_48dp); .getDimensionPixelSize(R.dimen.avatar_radius_48dp);

View File

@ -34,8 +34,8 @@ import com.keylesspalace.tusky.util.ImageLoadingHelper;
public class BlocksAdapter extends AccountAdapter { public class BlocksAdapter extends AccountAdapter {
public BlocksAdapter(AccountActionListener accountActionListener) { public BlocksAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener); super(accountActionListener, animateAvatar, animateEmojis);
} }
@NonNull @NonNull
@ -60,7 +60,7 @@ public class BlocksAdapter extends AccountAdapter {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder; BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }
@ -71,7 +71,6 @@ public class BlocksAdapter extends AccountAdapter {
private TextView displayName; private TextView displayName;
private ImageButton unblock; private ImageButton unblock;
private String id; private String id;
private boolean animateAvatar;
BlockedUserViewHolder(View itemView) { BlockedUserViewHolder(View itemView) {
super(itemView); super(itemView);
@ -79,14 +78,12 @@ public class BlocksAdapter extends AccountAdapter {
username = itemView.findViewById(R.id.blocked_user_username); username = itemView.findViewById(R.id.blocked_user_username);
displayName = itemView.findViewById(R.id.blocked_user_display_name); displayName = itemView.findViewById(R.id.blocked_user_display_name);
unblock = itemView.findViewById(R.id.blocked_user_unblock); unblock = itemView.findViewById(R.id.blocked_user_unblock);
animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext())
.getBoolean("animateGifAvatars", false);
} }
void setupWithAccount(Account account) { void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) {
id = account.getId(); id = account.getId();
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
displayName.setText(emojifiedName); displayName.setText(emojifiedName);
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());

View File

@ -27,8 +27,8 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
/** Both for follows and following lists. */ /** Both for follows and following lists. */
public class FollowAdapter extends AccountAdapter { public class FollowAdapter extends AccountAdapter {
public FollowAdapter(AccountActionListener accountActionListener) { public FollowAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener); super(accountActionListener, animateAvatar, animateEmojis);
} }
@NonNull @NonNull
@ -53,7 +53,7 @@ public class FollowAdapter extends AccountAdapter {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
AccountViewHolder holder = (AccountViewHolder) viewHolder; AccountViewHolder holder = (AccountViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }

View File

@ -10,27 +10,24 @@ 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.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.unicodeWrap
import com.keylesspalace.tusky.util.visible
import kotlinx.android.synthetic.main.item_follow_request_notification.view.* import kotlinx.android.synthetic.main.item_follow_request_notification.view.*
internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) { internal class FollowRequestViewHolder(
itemView: View,
private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) {
private var id: String? = null private var id: String? = null
private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context)
.getBoolean("animateGifAvatars", false)
fun setupWithAccount(account: Account) { fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
id = account.id id = account.id
val wrappedName = account.name.unicodeWrap() val wrappedName = account.name.unicodeWrap()
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView) val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
itemView.displayNameTextView.text = emojifiedName itemView.displayNameTextView.text = emojifiedName
if (showHeader) { if (showHeader) {
val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName) val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName)
itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply { itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply {
setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}.emojify(account.emojis, itemView) }.emojify(account.emojis, itemView, animateEmojis)
} }
itemView.notificationTextView?.visible(showHeader) itemView.notificationTextView?.visible(showHeader)
val format = itemView.context.getString(R.string.status_username_format) val format = itemView.context.getString(R.string.status_username_format)

View File

@ -27,8 +27,8 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
public class FollowRequestsAdapter extends AccountAdapter { public class FollowRequestsAdapter extends AccountAdapter {
public FollowRequestsAdapter(AccountActionListener accountActionListener) { public FollowRequestsAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener); super(accountActionListener, animateAvatar, animateEmojis);
} }
@NonNull @NonNull
@ -53,7 +53,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }

View File

@ -23,8 +23,8 @@ import java.util.HashMap;
public class MutesAdapter extends AccountAdapter { public class MutesAdapter extends AccountAdapter {
private HashMap<String, Boolean> mutingNotificationsMap; private HashMap<String, Boolean> mutingNotificationsMap;
public MutesAdapter(AccountActionListener accountActionListener) { public MutesAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener); super(accountActionListener, animateAvatar, animateEmojis);
mutingNotificationsMap = new HashMap<String, Boolean>(); mutingNotificationsMap = new HashMap<String, Boolean>();
} }
@ -51,7 +51,7 @@ public class MutesAdapter extends AccountAdapter {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder; MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
Account account = accountList.get(position); Account account = accountList.get(position);
holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId())); holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId()), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }
@ -73,7 +73,6 @@ public class MutesAdapter extends AccountAdapter {
private ImageButton unmute; private ImageButton unmute;
private ImageButton muteNotifications; private ImageButton muteNotifications;
private String id; private String id;
private boolean animateAvatar;
private boolean notifications; private boolean notifications;
MutedUserViewHolder(View itemView) { MutedUserViewHolder(View itemView) {
@ -83,13 +82,11 @@ public class MutesAdapter extends AccountAdapter {
displayName = itemView.findViewById(R.id.muted_user_display_name); displayName = itemView.findViewById(R.id.muted_user_display_name);
unmute = itemView.findViewById(R.id.muted_user_unmute); unmute = itemView.findViewById(R.id.muted_user_unmute);
muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications); muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications);
animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext())
.getBoolean("animateGifAvatars", false);
} }
void setupWithAccount(Account account, Boolean mutingNotifications) { void setupWithAccount(Account account, Boolean mutingNotifications, boolean animateAvatar, boolean animateEmojis) {
id = account.getId(); id = account.getId();
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
displayName.setText(emojifiedName); displayName.setText(emojifiedName);
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());

View File

@ -232,7 +232,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_FOLLOW_REQUEST: { case VIEW_TYPE_FOLLOW_REQUEST: {
if (payloadForHolder == null) { if (payloadForHolder == null) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(concreteNotificaton.getAccount()); holder.setupWithAccount(concreteNotificaton.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis());
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }
@ -255,7 +255,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
statusDisplayOptions.useBlurhash(), statusDisplayOptions.useBlurhash(),
CardViewMode.NONE, CardViewMode.NONE,
statusDisplayOptions.confirmReblogs(), statusDisplayOptions.confirmReblogs(),
statusDisplayOptions.hideStats() statusDisplayOptions.hideStats(),
statusDisplayOptions.animateEmojis()
); );
} }
@ -336,13 +337,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
String format = context.getString(R.string.notification_follow_format); String format = context.getString(R.string.notification_follow_format);
String wrappedDisplayName = StringUtils.unicodeWrap(account.getName()); String wrappedDisplayName = StringUtils.unicodeWrap(account.getName());
String wholeMessage = String.format(format, wrappedDisplayName); String wholeMessage = String.format(format, wrappedDisplayName);
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message); CharSequence emojifiedMessage = CustomEmojiHelper.emojify(
wholeMessage, account.getEmojis(), message, statusDisplayOptions.animateEmojis()
);
message.setText(emojifiedMessage); message.setText(emojifiedMessage);
String username = context.getString(R.string.status_username_format, account.getUsername()); String username = context.getString(R.string.status_username_format, account.getUsername());
usernameView.setText(username); usernameView.setText(username);
CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(wrappedDisplayName, account.getEmojis(), usernameView); CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(
wrappedDisplayName, account.getEmojis(), usernameView, statusDisplayOptions.animateEmojis()
);
displayNameView.setText(emojifiedDisplayName); displayNameView.setText(emojifiedDisplayName);
@ -425,7 +430,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
} }
private void setDisplayName(String name, List<Emoji> emojis) { private void setDisplayName(String name, List<Emoji> emojis) {
CharSequence emojifiedName = CustomEmojiHelper.emojify(name, emojis, displayName); CharSequence emojifiedName = CustomEmojiHelper.emojify(name, emojis, displayName, statusDisplayOptions.animateEmojis());
displayName.setText(emojifiedName); displayName.setText(emojifiedName);
} }
@ -519,7 +524,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage); final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage);
str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(), str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
CharSequence emojifiedText = CustomEmojiHelper.emojify(str, notificationViewData.getAccount().getEmojis(), message); CharSequence emojifiedText = CustomEmojiHelper.emojify(
str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis()
);
message.setText(emojifiedText); message.setText(emojifiedText);
if (statusViewData != null) { if (statusViewData != null) {
@ -630,11 +637,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
statusContent.setFilters(NO_INPUT_FILTER); statusContent.setFilters(NO_INPUT_FILTER);
} }
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, statusContent); CharSequence emojifiedText = CustomEmojiHelper.emojify(
content, emojis, statusContent, statusDisplayOptions.animateEmojis()
);
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener); LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener);
CharSequence emojifiedContentWarning = CharSequence emojifiedContentWarning = CustomEmojiHelper.emojify(
CustomEmojiHelper.emojify(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView); statusViewData.getSpoilerText(),
statusViewData.getStatusEmojis(),
contentWarningDescriptionTextView,
statusDisplayOptions.animateEmojis()
);
contentWarningDescriptionTextView.setText(emojifiedContentWarning); contentWarningDescriptionTextView.setText(emojifiedContentWarning);
} }

View File

@ -38,6 +38,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
private var mode = RESULT private var mode = RESULT
private var emojis: List<Emoji> = emptyList() private var emojis: List<Emoji> = emptyList()
private var resultClickListener: View.OnClickListener? = null private var resultClickListener: View.OnClickListener? = null
private var animateEmojis = false
fun setup( fun setup(
options: List<PollOptionViewData>, options: List<PollOptionViewData>,
@ -45,13 +46,15 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
votersCount: Int?, votersCount: Int?,
emojis: List<Emoji>, emojis: List<Emoji>,
mode: Int, mode: Int,
resultClickListener: View.OnClickListener?) { resultClickListener: View.OnClickListener?,
animateEmojis: Boolean) {
this.pollOptions = options this.pollOptions = options
this.voteCount = voteCount this.voteCount = voteCount
this.votersCount = votersCount this.votersCount = votersCount
this.emojis = emojis this.emojis = emojis
this.mode = mode this.mode = mode
this.resultClickListener = resultClickListener this.resultClickListener = resultClickListener
this.animateEmojis = animateEmojis
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -81,7 +84,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
RESULT -> { RESULT -> {
val percent = calculatePercent(option.votesCount, votersCount, voteCount) val percent = calculatePercent(option.votesCount, votersCount, voteCount)
val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context) val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context)
.emojify(emojis, holder.resultTextView) .emojify(emojis, holder.resultTextView, animateEmojis)
holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
val level = percent * 100 val level = percent * 100
@ -90,7 +93,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
holder.resultTextView.setOnClickListener(resultClickListener) holder.resultTextView.setOnClickListener(resultClickListener)
} }
SINGLE -> { SINGLE -> {
val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton) val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton, animateEmojis)
holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText)
holder.radioButton.isChecked = option.selected holder.radioButton.isChecked = option.selected
holder.radioButton.setOnClickListener { holder.radioButton.setOnClickListener {
@ -101,7 +104,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
} }
} }
MULTIPLE -> { MULTIPLE -> {
val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox) val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox, animateEmojis)
holder.checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText)
holder.checkBox.isChecked = option.selected holder.checkBox.isChecked = option.selected
holder.checkBox.setOnCheckedChangeListener { _, isChecked -> holder.checkBox.setOnCheckedChangeListener { _, isChecked ->

View File

@ -181,8 +181,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected abstract int getMediaPreviewHeight(Context context); protected abstract int getMediaPreviewHeight(Context context);
protected void setDisplayName(String name, List<Emoji> customEmojis) { protected void setDisplayName(String name, List<Emoji> customEmojis, StatusDisplayOptions statusDisplayOptions) {
CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, displayName); CharSequence emojifiedName = CustomEmojiHelper.emojify(
name, customEmojis, displayName, statusDisplayOptions.animateEmojis()
);
displayName.setText(emojifiedName); displayName.setText(emojifiedName);
} }
@ -206,7 +208,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
final StatusActionListener listener) { final StatusActionListener listener) {
boolean sensitive = !TextUtils.isEmpty(spoilerText); boolean sensitive = !TextUtils.isEmpty(spoilerText);
if (sensitive) { if (sensitive) {
CharSequence emojiSpoiler = CustomEmojiHelper.emojify(spoilerText, emojis, contentWarningDescription); CharSequence emojiSpoiler = CustomEmojiHelper.emojify(
spoilerText, emojis, contentWarningDescription, statusDisplayOptions.animateEmojis()
);
contentWarningDescription.setText(emojiSpoiler); contentWarningDescription.setText(emojiSpoiler);
contentWarningDescription.setVisibility(View.VISIBLE); contentWarningDescription.setVisibility(View.VISIBLE);
contentWarningButton.setVisibility(View.VISIBLE); contentWarningButton.setVisibility(View.VISIBLE);
@ -245,7 +249,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
StatusDisplayOptions statusDisplayOptions, StatusDisplayOptions statusDisplayOptions,
final StatusActionListener listener) { final StatusActionListener listener) {
if (expanded) { if (expanded) {
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content); CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis());
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener); LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener);
for (int i = 0; i < mediaLabels.length; ++i) { for (int i = 0; i < mediaLabels.length; ++i) {
updateMediaLabel(i, sensitive, expanded); updateMediaLabel(i, sensitive, expanded);
@ -709,7 +713,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
StatusDisplayOptions statusDisplayOptions, StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) { @Nullable Object payloads) {
if (payloads == null) { if (payloads == null) {
setDisplayName(status.getUserFullName(), status.getAccountEmojis()); setDisplayName(status.getUserFullName(), status.getAccountEmojis(), statusDisplayOptions);
setUsername(status.getNickname()); setUsername(status.getNickname());
setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
setIsReply(status.getInReplyToId() != null); setIsReply(status.getInReplyToId() != null);
@ -927,12 +931,28 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
listener.onViewThread(position); listener.onViewThread(position);
} }
}; };
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT, viewThreadListener); pollAdapter.setup(
poll.getOptions(),
poll.getVotesCount(),
poll.getVotersCount(),
emojis,
PollAdapter.RESULT,
viewThreadListener,
statusDisplayOptions.animateEmojis()
);
pollButton.setVisibility(View.GONE); pollButton.setVisibility(View.GONE);
} else { } else {
// voting possible // voting possible
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, null); pollAdapter.setup(
poll.getOptions(),
poll.getVotesCount(),
poll.getVotersCount(),
emojis,
poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE,
null,
statusDisplayOptions.animateEmojis()
);
pollButton.setVisibility(View.VISIBLE); pollButton.setVisibility(View.VISIBLE);

View File

@ -66,7 +66,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
if (rebloggedByDisplayName == null) { if (rebloggedByDisplayName == null) {
hideStatusInfo(); hideStatusInfo();
} else { } else {
setRebloggedByDisplayName(rebloggedByDisplayName, status); setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions);
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition())); statusInfo.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition()));
} }
@ -75,11 +75,15 @@ public class StatusViewHolder extends StatusBaseViewHolder {
} }
private void setRebloggedByDisplayName(final CharSequence name, final StatusViewData.Concrete status) { private void setRebloggedByDisplayName(final CharSequence name,
final StatusViewData.Concrete status,
final StatusDisplayOptions statusDisplayOptions) {
Context context = statusInfo.getContext(); Context context = statusInfo.getContext();
CharSequence wrappedName = StringUtils.unicodeWrap(name); CharSequence wrappedName = StringUtils.unicodeWrap(name);
CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName); CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName);
CharSequence emojifiedText = CustomEmojiHelper.emojify(boostedText, status.getRebloggedByAccountEmojis(), statusInfo); CharSequence emojifiedText = CustomEmojiHelper.emojify(
boostedText, status.getRebloggedByAccountEmojis(), statusInfo, statusDisplayOptions.animateEmojis()
);
statusInfo.setText(emojifiedText); statusInfo.setText(emojifiedText);
statusInfo.setVisibility(View.VISIBLE); statusInfo.setVisibility(View.VISIBLE);
} }

View File

@ -66,7 +66,8 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
statusDisplayOptions.useBlurhash(), statusDisplayOptions.useBlurhash(),
statusDisplayOptions.cardViewMode(), statusDisplayOptions.cardViewMode(),
statusDisplayOptions.confirmReblogs(), statusDisplayOptions.confirmReblogs(),
statusDisplayOptions.hideStats() statusDisplayOptions.hideStats(),
statusDisplayOptions.animateEmojis()
); );
} }

View File

@ -42,7 +42,8 @@ interface AnnouncementActionListener: LinkListener {
class AnnouncementAdapter( class AnnouncementAdapter(
private var items: List<Announcement> = emptyList(), private var items: List<Announcement> = emptyList(),
private val listener: AnnouncementActionListener, private val listener: AnnouncementActionListener,
private val wellbeingEnabled: Boolean = false private val wellbeingEnabled: Boolean = false,
private val animateEmojis: Boolean = false
) : RecyclerView.Adapter<AnnouncementAdapter.AnnouncementViewHolder>() { ) : RecyclerView.Adapter<AnnouncementAdapter.AnnouncementViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder {
@ -99,7 +100,8 @@ class AnnouncementAdapter(
reaction.staticUrl ?: "", reaction.staticUrl ?: "",
null null
)), )),
this this,
animateEmojis
) )
isChecked = reaction.me isChecked = reaction.me

View File

@ -82,8 +82,9 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled) adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled, animateEmojis)
announcementsList.adapter = adapter announcementsList.adapter = adapter

View File

@ -69,6 +69,7 @@ import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -160,7 +161,7 @@ class ComposeActivity : BaseActivity(),
composeScheduleView.setDateTime(composeOptions?.scheduledAt) composeScheduleView.setDateTime(composeOptions?.scheduledAt)
} }
setupComposeField(viewModel.startingText) setupComposeField(preferences, viewModel.startingText)
setupContentWarningField(composeOptions?.contentWarning) setupContentWarningField(composeOptions?.contentWarning)
setupPollView() setupPollView()
applyShareIntent(intent, savedInstanceState) applyShareIntent(intent, savedInstanceState)
@ -245,13 +246,18 @@ class ComposeActivity : BaseActivity(),
composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() }
} }
private fun setupComposeField(startingText: String?) { private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
composeEditField.setOnCommitContentListener(this) composeEditField.setOnCommitContentListener(this)
composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
composeEditField.setAdapter( composeEditField.setAdapter(
ComposeAutoCompleteAdapter(this)) ComposeAutoCompleteAdapter(
this,
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
)
)
composeEditField.setTokenizer(ComposeTokenizer()) composeEditField.setTokenizer(ComposeTokenizer())
composeEditField.setText(startingText) composeEditField.setText(startingText)

View File

@ -53,11 +53,15 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
private final ArrayList<AutocompleteResult> resultList; private final ArrayList<AutocompleteResult> resultList;
private final AutocompletionProvider autocompletionProvider; private final AutocompletionProvider autocompletionProvider;
private final boolean animateAvatar;
private final boolean animateEmojis;
public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider) { public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider, boolean animateAvatar, boolean animateEmojis) {
super(); super();
resultList = new ArrayList<>(); resultList = new ArrayList<>();
this.autocompletionProvider = autocompletionProvider; this.autocompletionProvider = autocompletionProvider;
this.animateAvatar = animateAvatar;
this.animateEmojis = animateEmojis;
} }
@Override @Override
@ -147,15 +151,12 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
); );
accountViewHolder.username.setText(formattedUsername); accountViewHolder.username.setText(formattedUsername);
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(),
account.getEmojis(), accountViewHolder.displayName); account.getEmojis(), accountViewHolder.displayName, animateEmojis);
accountViewHolder.displayName.setText(emojifiedName); accountViewHolder.displayName.setText(emojifiedName);
int avatarRadius = accountViewHolder.avatar.getContext().getResources() int avatarRadius = accountViewHolder.avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_42dp); .getDimensionPixelSize(R.dimen.avatar_radius_42dp);
boolean animateAvatar = PreferenceManager.getDefaultSharedPreferences(accountViewHolder.avatar.getContext())
.getBoolean("animateGifAvatars", false);
ImageLoadingHelper.loadAvatar( ImageLoadingHelper.loadAvatar(
account.getAvatar(), account.getAvatar(),
accountViewHolder.avatar, accountViewHolder.avatar,

View File

@ -75,7 +75,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener); setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener);
setDisplayName(account.getDisplayName(), account.getEmojis()); setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
setUsername(account.getUsername()); setUsername(account.getUsername());
setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
setIsReply(status.getInReplyToId() != null); setIsReply(status.getInReplyToId() != null);

View File

@ -67,8 +67,8 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true), confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
) )
adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry) adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry)

View File

@ -174,6 +174,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.pref_title_enable_swipe_for_tabs) setTitle(R.string.pref_title_enable_swipe_for_tabs)
isSingleLineTitle = false isSingleLineTitle = false
} }
switchPreference {
setDefaultValue(false)
key = PrefKeys.ANIMATE_CUSTOM_EMOJIS
setTitle(R.string.pref_title_animate_custom_emojis)
isSingleLineTitle = false
}
} }
preferenceCategory(R.string.pref_title_browser_settings) { preferenceCategory(R.string.pref_title_browser_settings) {

View File

@ -75,7 +75,7 @@ class StatusViewHolder(
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
mediaViewHeight) mediaViewHeight)
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions.useAbsoluteTime) statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions)
setCreatedAt(status.createdAt) setCreatedAt(status.createdAt)
} }
@ -89,7 +89,7 @@ class StatusViewHolder(
itemView.statusContentWarningButton.hide() itemView.statusContentWarningButton.hide()
itemView.statusContentWarningDescription.hide() itemView.statusContentWarningDescription.hide()
} else { } else {
val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription) val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
itemView.statusContentWarningDescription.text = emojiSpoiler itemView.statusContentWarningDescription.text = emojiSpoiler
itemView.statusContentWarningDescription.show() itemView.statusContentWarningDescription.show()
itemView.statusContentWarningButton.show() itemView.statusContentWarningButton.show()
@ -122,7 +122,7 @@ class StatusViewHolder(
emojis: List<Emoji>, emojis: List<Emoji>,
listener: LinkListener) { listener: LinkListener) {
if (expanded) { if (expanded) {
val emojifiedText = content.emojify(emojis, itemView.statusContent) val emojifiedText = content.emojify(emojis, itemView.statusContent, statusDisplayOptions.animateEmojis)
LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener) LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener)
} else { } else {
LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener) LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener)

View File

@ -111,7 +111,8 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true), confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
) )
adapter = StatusesAdapter(statusDisplayOptions, adapter = StatusesAdapter(statusDisplayOptions,

View File

@ -25,7 +25,7 @@ import com.keylesspalace.tusky.adapter.AccountViewHolder
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
class SearchAccountsAdapter(private val linkListener: LinkListener) class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean)
: PagedListAdapter<Account, RecyclerView.ViewHolder>(ACCOUNT_COMPARATOR) { : PagedListAdapter<Account, RecyclerView.ViewHolder>(ACCOUNT_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@ -37,7 +37,7 @@ class SearchAccountsAdapter(private val linkListener: LinkListener)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item -> getItem(position)?.let { item ->
(holder as AccountViewHolder).apply { (holder as AccountViewHolder).apply {
setupWithAccount(item) setupWithAccount(item, animateAvatars, animateEmojis)
setupLinkListener(linkListener) setupLinkListener(linkListener)
} }
} }

View File

@ -18,12 +18,23 @@ package com.keylesspalace.tusky.components.search.fragments
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.paging.PagedListAdapter import androidx.paging.PagedListAdapter
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.Account
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import kotlinx.android.synthetic.main.fragment_search.*
class SearchAccountsFragment : SearchFragment<Account>() { class SearchAccountsFragment : SearchFragment<Account>() {
override fun createAdapter(): PagedListAdapter<Account, *> = SearchAccountsAdapter(this) override fun createAdapter(): PagedListAdapter<Account, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
return SearchAccountsAdapter(
this,
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
)
}
override val networkStateRefresh: LiveData<NetworkState> override val networkStateRefresh: LiveData<NetworkState>
get() = viewModel.networkStateAccountRefresh get() = viewModel.networkStateAccountRefresh

View File

@ -87,7 +87,8 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true), confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
) )
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL))

View File

@ -20,6 +20,7 @@ import android.util.Log
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -35,6 +36,7 @@ import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Relationship
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.util.HttpHeaderLink import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
@ -78,11 +80,15 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
val pm = PreferenceManager.getDefaultSharedPreferences(view.context)
val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
adapter = when (type) { adapter = when (type) {
Type.BLOCKS -> BlocksAdapter(this) Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis)
Type.MUTES -> MutesAdapter(this) Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis)
Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this) Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this, animateAvatar, animateEmojis)
else -> FollowAdapter(this) else -> FollowAdapter(this, animateAvatar, animateEmojis)
} }
recyclerView.adapter = adapter recyclerView.adapter = adapter

View File

@ -252,7 +252,8 @@ public class NotificationsFragment extends SFragment implements
preferences.getBoolean("useBlurhash", true), preferences.getBoolean("useBlurhash", true),
CardViewMode.NONE, CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true), preferences.getBoolean("confirmReblogs", true),
preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
); );
adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(), adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),

View File

@ -252,7 +252,8 @@ public class TimelineFragment extends SFragment implements
CardViewMode.INDENTED : CardViewMode.INDENTED :
CardViewMode.NONE, CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true), preferences.getBoolean("confirmReblogs", true),
preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
); );
adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this); adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this);

View File

@ -134,7 +134,8 @@ public final class ViewThreadFragment extends SFragment implements
CardViewMode.INDENTED : CardViewMode.INDENTED :
CardViewMode.NONE, CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true), preferences.getBoolean("confirmReblogs", true),
preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
); );
adapter = new ThreadAdapter(statusDisplayOptions, this); adapter = new ThreadAdapter(statusDisplayOptions, this);
} }

View File

@ -31,6 +31,7 @@ object PrefKeys {
const val SHOW_CARDS_IN_TIMELINES = "showCardsInTimelines" const val SHOW_CARDS_IN_TIMELINES = "showCardsInTimelines"
const val CONFIRM_REBLOGS = "confirmReblogs" const val CONFIRM_REBLOGS = "confirmReblogs"
const val ENABLE_SWIPE_FOR_TABS = "enableSwipeForTabs" const val ENABLE_SWIPE_FOR_TABS = "enableSwipeForTabs"
const val ANIMATE_CUSTOM_EMOJIS = "animateCustomEmojis"
const val CUSTOM_TABS = "customTabs" const val CUSTOM_TABS = "customTabs"
const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications" const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications"

View File

@ -16,11 +16,9 @@
@file:JvmName("CustomEmojiHelper") @file:JvmName("CustomEmojiHelper")
package com.keylesspalace.tusky.util package com.keylesspalace.tusky.util
import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.*
import android.graphics.drawable.Drawable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.ReplacementSpan import android.text.style.ReplacementSpan
import android.view.View import android.view.View
@ -33,6 +31,8 @@ import com.keylesspalace.tusky.entity.Emoji
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.regex.Pattern import java.util.regex.Pattern
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.settings.PrefKeys
/** /**
* replaces emoji shortcodes in a text with EmojiSpans * replaces emoji shortcodes in a text with EmojiSpans
@ -41,7 +41,7 @@ import java.util.regex.Pattern
* @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable) * @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable)
* @return the text with the shortcodes replaced by EmojiSpans * @return the text with the shortcodes replaced by EmojiSpans
*/ */
fun CharSequence.emojify(emojis: List<Emoji>?, view: View) : CharSequence { fun CharSequence.emojify(emojis: List<Emoji>?, view: View, animate: Boolean) : CharSequence {
if(emojis.isNullOrEmpty()) if(emojis.isNullOrEmpty())
return this return this
@ -56,9 +56,9 @@ fun CharSequence.emojify(emojis: List<Emoji>?, view: View) : CharSequence {
builder.setSpan(span, matcher.start(), matcher.end(), 0) builder.setSpan(span, matcher.start(), matcher.end(), 0)
Glide.with(view) Glide.with(view)
.asBitmap() .asDrawable()
.load(url) .load(url)
.into(span.getTarget()) .into(span.getTarget(animate))
} }
} }
return builder return builder
@ -97,11 +97,29 @@ class EmojiSpan(val viewWeakReference: WeakReference<View>) : ReplacementSpan()
} }
} }
fun getTarget(): Target<Bitmap> { fun getTarget(animate : Boolean): Target<Drawable> {
return object : CustomTarget<Bitmap>() { return object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
viewWeakReference.get()?.let { view -> viewWeakReference.get()?.let { view ->
imageDrawable = BitmapDrawable(view.context.resources, resource) if(animate && resource is Animatable) {
val callback = resource.callback
resource.callback = object: Drawable.Callback {
override fun unscheduleDrawable(p0: Drawable, p1: Runnable) {
callback?.unscheduleDrawable(p0, p1)
}
override fun scheduleDrawable(p0: Drawable, p1: Runnable, p2: Long) {
callback?.scheduleDrawable(p0, p1, p2)
}
override fun invalidateDrawable(p0: Drawable) {
callback?.invalidateDrawable(p0)
view.invalidate()
}
}
resource.start()
}
imageDrawable = resource
view.invalidate() view.invalidate()
} }
} }

View File

@ -16,5 +16,7 @@ data class StatusDisplayOptions(
@get:JvmName("confirmReblogs") @get:JvmName("confirmReblogs")
val confirmReblogs: Boolean, val confirmReblogs: Boolean,
@get:JvmName("hideStats") @get:JvmName("hideStats")
val hideStats: Boolean val hideStats: Boolean,
@get:JvmName("animateEmojis")
val animateEmojis: Boolean
) )

View File

@ -243,7 +243,7 @@ class StatusViewHelper(private val itemView: View) {
} }
} }
fun setupPollReadonly(poll: PollViewData?, emojis: List<Emoji>, useAbsoluteTime: Boolean) { fun setupPollReadonly(poll: PollViewData?, emojis: List<Emoji>, statusDisplayOptions: StatusDisplayOptions) {
val pollResults = listOf<TextView>( val pollResults = listOf<TextView>(
itemView.findViewById(R.id.status_poll_option_result_0), itemView.findViewById(R.id.status_poll_option_result_0),
itemView.findViewById(R.id.status_poll_option_result_1), itemView.findViewById(R.id.status_poll_option_result_1),
@ -261,10 +261,10 @@ class StatusViewHelper(private val itemView: View) {
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
setupPollResult(poll, emojis, pollResults) setupPollResult(poll, emojis, pollResults, statusDisplayOptions.animateEmojis)
pollDescription.visibility = View.VISIBLE pollDescription.visibility = View.VISIBLE
pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, useAbsoluteTime) pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, statusDisplayOptions.useAbsoluteTime)
} }
} }
@ -292,7 +292,7 @@ class StatusViewHelper(private val itemView: View) {
} }
private fun setupPollResult(poll: PollViewData, emojis: List<Emoji>, pollResults: List<TextView>) { private fun setupPollResult(poll: PollViewData, emojis: List<Emoji>, pollResults: List<TextView>, animateEmojis: Boolean) {
val options = poll.options val options = poll.options
for (i in 0 until Status.MAX_POLL_OPTIONS) { for (i in 0 until Status.MAX_POLL_OPTIONS) {
@ -300,7 +300,7 @@ class StatusViewHelper(private val itemView: View) {
val percent = calculatePercent(options[i].votesCount, poll.votersCount, poll.votesCount) val percent = calculatePercent(options[i].votesCount, poll.votersCount, poll.votesCount)
val pollOptionText = buildDescription(options[i].title, percent, pollResults[i].context) val pollOptionText = buildDescription(options[i].title, percent, pollResults[i].context)
pollResults[i].text = pollOptionText.emojify(emojis, pollResults[i]) pollResults[i].text = pollOptionText.emojify(emojis, pollResults[i], animateEmojis)
pollResults[i].visibility = View.VISIBLE pollResults[i].visibility = View.VISIBLE
val level = percent * 100 val level = percent * 100

View File

@ -243,6 +243,7 @@
<string name="pref_title_bot_overlay">Show indicator for bots</string> <string name="pref_title_bot_overlay">Show indicator for bots</string>
<string name="pref_title_animate_gif_avatars">Animate GIF avatars</string> <string name="pref_title_animate_gif_avatars">Animate GIF avatars</string>
<string name="pref_title_gradient_for_media">Show colorful gradients for hidden media</string> <string name="pref_title_gradient_for_media">Show colorful gradients for hidden media</string>
<string name="pref_title_animate_custom_emojis">Animate custom emojis</string>
<string name="pref_title_status_filter">Timeline filtering</string> <string name="pref_title_status_filter">Timeline filtering</string>
<string name="pref_title_status_tabs">Tabs</string> <string name="pref_title_status_tabs">Tabs</string>