diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Status.java b/app/src/main/java/app/fedilab/android/client/entities/api/Status.java index e3c753055..773022837 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Status.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Status.java @@ -115,6 +115,10 @@ public class Status implements Serializable, Cloneable { public transient boolean submitted = false; public transient boolean spoilerChecked = false; public Filter filteredByApp; + public transient Spannable contentSpan; + public transient Spannable contentSpoilerSpan; + public transient Spannable contentTranslateSpan; + @Override public boolean equals(@Nullable Object obj) { boolean same = false; @@ -124,17 +128,29 @@ public class Status implements Serializable, Cloneable { return same; } - public synchronized Spannable getSpanContent(Context context, WeakReference viewWeakReference) { - return SpannableHelper.convert(context, content, this, null, null, true, viewWeakReference); + public synchronized Spannable getSpanContent(Context context, WeakReference viewWeakReference, Callback callback) { + if (contentSpan == null) { + contentSpan = SpannableHelper.convert(context, content, this, null, null, true, viewWeakReference, callback); + } + return contentSpan; } - - public synchronized Spannable getSpanSpoiler(Context context, WeakReference viewWeakReference) { - return SpannableHelper.convert(context, spoiler_text, this, null, null, true, viewWeakReference); + public synchronized Spannable getSpanSpoiler(Context context, WeakReference viewWeakReference, Callback callback) { + if (contentSpoilerSpan == null) { + contentSpoilerSpan = SpannableHelper.convert(context, spoiler_text, this, null, null, true, viewWeakReference, callback); + } + return contentSpoilerSpan; } - public synchronized Spannable getSpanTranslate(Context context, WeakReference viewWeakReference) { - return SpannableHelper.convert(context, translationContent, this, null, null, true, viewWeakReference); + public synchronized Spannable getSpanTranslate(Context context, WeakReference viewWeakReference, Callback callback) { + if (contentTranslateSpan == null) { + contentTranslateSpan = SpannableHelper.convert(context, translationContent, this, null, null, true, viewWeakReference, callback); + } + return contentTranslateSpan; + } + + public interface Callback { + void emojiFetched(); } @NonNull diff --git a/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java b/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java index 108351115..39689d82d 100644 --- a/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java +++ b/app/src/main/java/app/fedilab/android/helper/CustomEmoji.java @@ -6,6 +6,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; +import android.text.SpannableStringBuilder; import android.text.style.ReplacementSpan; import android.view.View; @@ -13,26 +14,53 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; +import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import java.lang.ref.WeakReference; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import app.fedilab.android.R; +import app.fedilab.android.client.entities.api.Emoji; +import app.fedilab.android.client.entities.api.Status; public class CustomEmoji extends ReplacementSpan { private final float scale; private final WeakReference viewWeakReference; private Drawable imageDrawable; - + private boolean callbackCalled; CustomEmoji(WeakReference viewWeakReference) { Context mContext = viewWeakReference.get().getContext(); this.viewWeakReference = viewWeakReference; SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(mContext); scale = sharedpreferences.getFloat(mContext.getString(R.string.SET_FONT_SCALE), 1.1f); + callbackCalled = false; + } + + public SpannableStringBuilder makeEmoji(SpannableStringBuilder content, List emojiList, boolean animate, Status.Callback callback) { + if (emojiList != null && emojiList.size() > 0) { + int count = 1; + for (Emoji emoji : emojiList) { + Matcher matcher = Pattern.compile(":" + emoji.shortcode + ":", Pattern.LITERAL) + .matcher(content); + while (matcher.find()) { + CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(viewWeakReference.get())); + content.setSpan(customEmoji, matcher.start(), matcher.end(), 0); + Glide.with(viewWeakReference.get()) + .asDrawable() + .load(animate ? emoji.url : emoji.static_url) + .into(customEmoji.getTarget(animate, count == emojiList.size() && !callbackCalled ? callback : null)); + } + count++; + } + } + return content; } @Override @@ -61,7 +89,7 @@ public class CustomEmoji extends ReplacementSpan { } } - public Target getTarget(boolean animate) { + public Target getTarget(boolean animate, Status.Callback callback) { return new CustomTarget() { @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { @@ -97,6 +125,10 @@ public class CustomEmoji extends ReplacementSpan { if (view != null) { view.invalidate(); } + if (callback != null) { + callbackCalled = true; + callback.emojiFetched(); + } } @Override diff --git a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java index 455614b3a..af12f71d0 100644 --- a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java @@ -89,10 +89,16 @@ public class SpannableHelper { public static final String CLICKABLE_SPAN = "CLICKABLE_SPAN"; + public static Spannable convert(Context context, String text, + Status status, Account account, Announcement announcement, + boolean convertHtml, WeakReference viewWeakReference) { + return convert(context, text, status, account, announcement, convertHtml, viewWeakReference, null); + } + public static Spannable convert(Context context, String text, Status status, Account account, Announcement announcement, boolean convertHtml, - WeakReference viewWeakReference) { + WeakReference viewWeakReference, Status.Callback callback) { SpannableString initialContent; @@ -161,20 +167,8 @@ public class SpannableHelper { } SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); boolean animate = !sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_ANIMATED_EMOJI), false); - if (emojiList != null && emojiList.size() > 0) { - for (Emoji emoji : emojiList) { - Matcher matcher = Pattern.compile(":" + emoji.shortcode + ":", Pattern.LITERAL) - .matcher(content); - while (matcher.find()) { - CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); - content.setSpan(customEmoji, matcher.start(), matcher.end(), 0); - Glide.with(view) - .asDrawable() - .load(animate ? emoji.url : emoji.static_url) - .into(customEmoji.getTarget(animate)); - } - } - } + CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); + content = customEmoji.makeEmoji(content, emojiList, animate, callback); if (imagesToReplace.size() > 0) { for (Map.Entry entry : imagesToReplace.entrySet()) { @@ -183,12 +177,11 @@ public class SpannableHelper { Matcher matcher = Pattern.compile(key, Pattern.LITERAL) .matcher(content); while (matcher.find()) { - CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); content.setSpan(customEmoji, matcher.start(), matcher.end(), 0); Glide.with(view) .asDrawable() .load(url) - .into(customEmoji.getTarget(animate)); + .into(customEmoji.getTarget(animate, null)); } } @@ -1066,7 +1059,7 @@ public class SpannableHelper { Glide.with(viewWeakReference.get()) .asDrawable() .load(animate ? emoji.url : emoji.static_url) - .into(customEmoji.getTarget(animate)); + .into(customEmoji.getTarget(animate, null)); } } } diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java index 323267202..d01e12acb 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java @@ -1107,7 +1107,7 @@ public class ComposeAdapter extends RecyclerView.Adapter(holder.binding.statusContent)), + new WeakReference<>(holder.binding.statusContent), null), TextView.BufferType.SPANNABLE); MastodonHelper.loadPPMastodon(holder.binding.avatar, status.account); if (status.account != null) { @@ -1122,7 +1122,7 @@ public class ComposeAdapter extends RecyclerView.Adapter(holder.binding.spoiler)), + new WeakReference<>(holder.binding.spoiler), null), TextView.BufferType.SPANNABLE); } else { holder.binding.spoiler.setVisibility(View.GONE); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java index ed5c8b617..f163d4b9f 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java @@ -151,7 +151,7 @@ public class ConversationAdapter extends RecyclerView.Adapter(holder.binding.spoiler)), + new WeakReference<>(holder.binding.spoiler), () -> notifyItemChanged(holder.getBindingAdapterPosition())), TextView.BufferType.SPANNABLE); } else { holder.binding.spoiler.setVisibility(View.GONE); @@ -161,7 +161,7 @@ public class ConversationAdapter extends RecyclerView.Adapter(holder.binding.statusContent)), + new WeakReference<>(holder.binding.statusContent), () -> notifyItemChanged(holder.getBindingAdapterPosition())), TextView.BufferType.SPANNABLE); //--- DATE --- holder.binding.lastMessageDate.setText(Helper.dateDiff(context, conversation.last_status.created_at)); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java index 78ed4255c..4f0ac429d 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java @@ -71,6 +71,7 @@ public class NotificationAdapter extends RecyclerView.Adapter notificationList) { this.notificationList = notificationList; @@ -120,6 +121,13 @@ public class NotificationAdapter extends RecyclerView.Adapter public FetchMoreCallBack fetchMoreCallBack; private Context context; + private RecyclerView mRecyclerView; + public StatusAdapter(List statuses, Timeline.TimeLineEnum timelineType, boolean minified, boolean canBeFederated, boolean checkRemotely) { this.statusList = statuses; this.timelineType = timelineType; @@ -355,6 +357,7 @@ public class StatusAdapter extends RecyclerView.Adapter StatusesVM statusesVM, SearchVM searchVM, StatusViewHolder holder, + RecyclerView recyclerView, RecyclerView.Adapter adapter, List statusList, Status status, @@ -986,7 +989,9 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.spoiler.setVisibility(View.VISIBLE); holder.binding.spoiler.setText( statusToDeal.getSpanSpoiler(context, - new WeakReference<>(holder.binding.spoiler)), + new WeakReference<>(holder.binding.spoiler), () -> { + recyclerView.post(() -> adapter.notifyItemChanged(holder.getBindingAdapterPosition())); + }), TextView.BufferType.SPANNABLE); statusToDeal.isExpended = true; statusToDeal.isMediaDisplayed = true; @@ -1001,7 +1006,9 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.spoiler.setText( statusToDeal.getSpanSpoiler(context, - new WeakReference<>(holder.binding.spoiler)), + new WeakReference<>(holder.binding.spoiler), () -> { + recyclerView.post(() -> adapter.notifyItemChanged(holder.getBindingAdapterPosition())); + }), TextView.BufferType.SPANNABLE); } if (statusToDeal.isExpended) { @@ -1049,7 +1056,9 @@ public class StatusAdapter extends RecyclerView.Adapter //--- MAIN CONTENT --- holder.binding.statusContent.setText( statusToDeal.getSpanContent(context, - new WeakReference<>(holder.binding.statusContent)), + new WeakReference<>(holder.binding.statusContent), () -> { + recyclerView.post(() -> adapter.notifyItemChanged(holder.getBindingAdapterPosition())); + }), TextView.BufferType.SPANNABLE); if (truncate_toots_size > 0) { holder.binding.statusContent.setMaxLines(truncate_toots_size); @@ -1085,7 +1094,9 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.containerTrans.setVisibility(View.VISIBLE); holder.binding.statusContentTranslated.setText( statusToDeal.getSpanTranslate(context, - new WeakReference<>(holder.binding.statusContentTranslated)), + new WeakReference<>(holder.binding.statusContentTranslated), () -> { + recyclerView.post(() -> adapter.notifyItemChanged(holder.getBindingAdapterPosition())); + }), TextView.BufferType.SPANNABLE); } else { holder.binding.containerTrans.setVisibility(View.GONE); @@ -2087,6 +2098,13 @@ public class StatusAdapter extends RecyclerView.Adapter } + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + + mRecyclerView = recyclerView; + } + private static boolean mediaObfuscated(Status status) { //Media is not sensitive and doesn't have a spoiler text if (!status.isMediaObfuscated) { @@ -2200,7 +2218,7 @@ public class StatusAdapter extends RecyclerView.Adapter StatusViewHolder holder = (StatusViewHolder) viewHolder; StatusesVM statusesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(StatusesVM.class); SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class); - statusManagement(context, statusesVM, searchVM, holder, this, statusList, status, timelineType, minified, canBeFederated, checkRemotely, fetchMoreCallBack); + statusManagement(context, statusesVM, searchVM, holder, mRecyclerView, this, statusList, status, timelineType, minified, canBeFederated, checkRemotely, fetchMoreCallBack); } else if (viewHolder.getItemViewType() == STATUS_FILTERED_HIDE) { StatusViewHolder holder = (StatusViewHolder) viewHolder; diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/StatusHistoryAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/StatusHistoryAdapter.java index 66c2504ca..33dd4c86f 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/StatusHistoryAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/StatusHistoryAdapter.java @@ -56,13 +56,13 @@ public class StatusHistoryAdapter extends RecyclerView.Adapter(holder.binding.statusContent)), + new WeakReference<>(holder.binding.statusContent), null), TextView.BufferType.SPANNABLE); if (status.spoiler_text != null && !status.spoiler_text.trim().isEmpty()) { holder.binding.spoiler.setVisibility(View.VISIBLE); holder.binding.spoiler.setText( status.getSpanSpoiler(context, - new WeakReference<>(holder.binding.spoiler)), + new WeakReference<>(holder.binding.spoiler), null), TextView.BufferType.SPANNABLE); } else { holder.binding.spoiler.setVisibility(View.GONE);