From 7a51666a8bfe56bae3299dc386cd778236143874 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Thu, 3 Oct 2024 22:20:38 -0500 Subject: [PATCH] Show custom emoji reactions and allow sending them --- .../com/cheogram/android/EmojiSearch.java | 55 ++++++++++-- .../conversations/entities/Conversation.java | 77 +++++++++++++---- .../siacs/conversations/entities/Message.java | 8 +- .../conversations/entities/Reaction.java | 61 +++++++++++--- .../conversations/ui/BindingAdapters.java | 41 ++++----- .../ui/adapter/MessageAdapter.java | 83 ++++++++++++++----- 6 files changed, 246 insertions(+), 79 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/EmojiSearch.java b/src/cheogram/java/com/cheogram/android/EmojiSearch.java index fc37a7d1488a04ffa6ec244307090767ee044a30..1d4a3f5719f89d3e11969f0630a97e0bd4e64883 100644 --- a/src/cheogram/java/com/cheogram/android/EmojiSearch.java +++ b/src/cheogram/java/com/cheogram/android/EmojiSearch.java @@ -14,9 +14,13 @@ import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.chip.Chip; +import com.google.android.material.color.MaterialColors; import com.google.common.collect.Lists; import com.google.common.io.CharStreams; +import io.ipfs.cid.Cid; + import java.io.IOException; import java.io.InputStreamReader; import java.lang.Comparable; @@ -24,6 +28,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.PriorityQueue; import java.util.Set; import java.util.TreeSet; @@ -39,6 +44,7 @@ import org.json.JSONObject; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.EmojiSearchRowBinding; +import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; public class EmojiSearch { @@ -156,6 +162,19 @@ public class EmojiSearch { return new SpannableStringBuilder(unicode); } + public void setupChip(Chip chip, int count) { + if (count < 2) { + chip.setText(unicode); + } else { + chip.setText(String.format(Locale.ENGLISH, "%s %d", unicode, count)); + } + } + + @Override + public String toString() { + return unicode; + } + public String uniquePart() { return unicode; } @@ -173,10 +192,15 @@ public class EmojiSearch { return uniquePart().equals(((Emoji) o).uniquePart()); } + + @Override + public int hashCode() { + return uniquePart().hashCode(); + } } public static class CustomEmoji extends Emoji { - protected final String source; + public final String source; protected final Drawable icon; public CustomEmoji(final String shortcode, final String source, final Drawable icon, final String tag) { @@ -185,21 +209,40 @@ public class EmojiSearch { if (tag != null) tags.add(tag); this.source = source; this.icon = icon; - if (icon == null) { - throw new IllegalArgumentException("icon must not be null"); - } } public SpannableStringBuilder toInsert() { - SpannableStringBuilder builder = new SpannableStringBuilder(":" + shortcodes.get(0) + ":"); - builder.setSpan(new InlineImageSpan(icon, source), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + SpannableStringBuilder builder = new SpannableStringBuilder(toString()); + if (icon != null) builder.setSpan(new InlineImageSpan(icon, source), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return builder; } + public void setupChip(Chip chip, int count) { + if (icon == null) { + chip.setChipIconResource(R.drawable.ic_photo_24dp); + chip.setChipIconTint( + MaterialColors.getColorStateListOrNull( + chip.getContext(), + com.google.android.material.R.attr.colorOnSurface)); + } else { + SpannableStringBuilder builder = new SpannableStringBuilder("😇"); // needs to be same size as an emoji + if (icon != null) builder.setSpan(new InlineImageSpan(icon, source), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + chip.setText(builder); // We cannot use icon because it is a hardware bitmap + } + if (count > 1) { + chip.append(String.format(Locale.ENGLISH, " %d", count)); + } + } + @Override public String uniquePart() { return source; } + + @Override + public String toString() { + return ":" + shortcodes.get(0) + ":"; + } } public class EmojiSearchAdapter extends ListAdapter { diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index d410c30fe46e719a2da313a0492a654a29f580e4..3240ecdd94247486ea8e1775042e8cc16af0dd76 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -23,6 +23,7 @@ import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.style.ImageSpan; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.Gravity; @@ -64,7 +65,9 @@ import androidx.viewpager.widget.ViewPager; import com.caverock.androidsvg.SVG; +import com.cheogram.android.BobTransfer; import com.cheogram.android.ConversationPage; +import com.cheogram.android.GetThumbnailForCid; import com.cheogram.android.Util; import com.cheogram.android.WebxdcPage; @@ -106,6 +109,7 @@ import java.util.stream.Collectors; import java.util.Set; import java.util.Timer; import java.util.TimerTask; +import java.util.function.Function; import me.saket.bettermovementmethod.BetterLinkMovementMethod; @@ -293,6 +297,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl for (int i = messages.size() - 1; i >= 0; --i) { final Message message = messages.get(i); if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue; + if (asReaction(message) != null) continue; if (message.isRead()) { return first; } else { @@ -308,6 +313,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl synchronized (this.messages) { for (final Message message : Lists.reverse(this.messages)) { if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue; + if (asReaction(message) != null) continue; if (message.getStatus() == Message.STATUS_RECEIVED) { final String serverMsgId = message.getServerMsgId(); if (serverMsgId != null && multi) { @@ -708,26 +714,59 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl thread.first = m; } } - final var reply = m.getReply(); - if (reply != null && reply.getAttribute("id") != null) { - extraIds.add(reply.getAttribute("id")); - final var body = m.getBody(true).toString().replaceAll("\\s", ""); - if (Emoticons.isEmoji(body)) { - reactions.put(reply.getAttribute("id"), new Reaction(body, true, m.getCounterpart(), m.getTrueCounterpart(), m.getOccupantId())); - iterator.remove(); - } + final var asReaction = asReaction(m); + if (asReaction != null) { + reactions.put(asReaction.first, asReaction.second); + iterator.remove(); } if (m.wasMergedIntoPrevious(xmppConnectionService) || (m.getSubject() != null && !m.isOOb() && (m.getRawBody() == null || m.getRawBody().length() == 0)) || (getLockThread() && !extraIds.contains(m.replyId()) && (mthread == null || !mthread.getContent().equals(getThread() == null ? "" : getThread().getContent())))) { iterator.remove(); } else if (getLockThread() && mthread != null) { + final var reply = m.getReply(); + if (reply != null && reply.getAttribute("id") != null) extraIds.add(reply.getAttribute("id")); Element reactions = m.getReactionsEl(); if (reactions != null && reactions.getAttribute("id") != null) extraIds.add(reactions.getAttribute("id")); } } } - public Reaction.Aggregated aggregatedReactionsFor(Message m) { + protected Pair asReaction(Message m) { + final var reply = m.getReply(); + if (reply != null && reply.getAttribute("id") != null) { + final var body = m.getBody(true).toString().replaceAll("\\s", ""); + if (Emoticons.isEmoji(body)) { + return new Pair<>(reply.getAttribute("id"), new Reaction(body, null, m.getStatus() <= Message.STATUS_RECEIVED, m.getCounterpart(), m.getTrueCounterpart(), m.getOccupantId())); + } else { + final var html = m.getHtml(); + if (html == null) return null; + + SpannableStringBuilder spannable = m.getSpannableBody(null, null, false); + ImageSpan[] imageSpans = spannable.getSpans(0, spannable.length(), ImageSpan.class); + for (ImageSpan span : imageSpans) { + final int start = spannable.getSpanStart(span); + final int end = spannable.getSpanEnd(span); + spannable.delete(start, end); + } + if (imageSpans.length == 1 && spannable.toString().replaceAll("\\s", "").length() < 1) { + // Only one inline image, so it's a custom emoji by itself as a reply/reaction + final var source = imageSpans[0].getSource(); + var shortcode = ""; + final var img = html.findChild("img"); + if (img != null) { + shortcode = img.getAttribute("alt").replaceAll("(^:)|(:$)", ""); + } + if (source != null && source.length() > 0 && source.substring(0, 4).equals("cid:")) { + final Cid cid = BobTransfer.cid(Uri.parse(source)); + return new Pair<>(reply.getAttribute("id"), new Reaction(shortcode, cid, m.getStatus() <= Message.STATUS_RECEIVED, m.getCounterpart(), m.getTrueCounterpart(), m.getOccupantId())); + } + } + } + } + return null; + } + + public Reaction.Aggregated aggregatedReactionsFor(Message m, Function thumbnailer) { Set result = new HashSet<>(); if (getMode() == MODE_MULTI) { result.addAll(reactions.get(m.getServerMsgId())); @@ -737,7 +776,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl result.addAll(reactions.get(m.getRemoteMsgId())); } result.addAll(m.getReactions()); - return Reaction.aggregated(result); + return Reaction.aggregated(result, thumbnailer); } public Thread getThread(String id) { @@ -899,15 +938,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public Message getLatestMessage() { synchronized (this.messages) { - if (this.messages.size() == 0) { - Message message = new Message(this, "", Message.ENCRYPTION_NONE); - message.setType(Message.TYPE_STATUS); - message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp())); - message.setTimeReceived(Math.max(getCreated(), getLastClearHistory().getTimestamp())); + for(final Message message : Lists.reverse(this.messages)) { + if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue; + if (asReaction(message) != null) continue; return message; - } else { - return this.messages.get(this.messages.size() - 1); } + + Message message = new Message(this, "", Message.ENCRYPTION_NONE); + message.setType(Message.TYPE_STATUS); + message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp())); + message.setTimeReceived(Math.max(getCreated(), getLastClearHistory().getTimestamp())); + return message; } } @@ -1369,6 +1410,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl int count = 0; for(final Message message : Lists.reverse(this.messages)) { if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue; + if (asReaction(message) != null) continue; final boolean muted = xmppConnectionService != null && message.getStatus() == Message.STATUS_RECEIVED && getMode() == Conversation.MODE_MULTI && xmppConnectionService.isMucUserMuted(new MucOptions.User(null, getJid(), message.getOccupantId(), null, null)); if (muted) continue; if (message.isRead()) { @@ -1388,6 +1430,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl synchronized (this.messages) { for (Message message : messages) { if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue; + if (asReaction(message) != null) continue; if (message.getStatus() == Message.STATUS_RECEIVED) { ++count; } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 88e2d6ae7c211094d0ab45c129c585c9d3d7c2fe..8cb45294314c25bd55ed03fec170c1c26706ffc7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -1084,10 +1084,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) { + return getSpannableBody(thumbnailer, fallbackImg, true); + } + + public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg, final boolean includeReplyTo) { SpannableStringBuilder spannableBody; final Element html = getHtml(); if (html == null || Build.VERSION.SDK_INT < 24) { - spannableBody = new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody(getInReplyTo() != null)).trim()); + spannableBody = new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody(includeReplyTo && getInReplyTo() != null)).trim()); spannableBody.setSpan(PLAIN_TEXT_SPAN, 0, spannableBody.length(), 0); // Let adapter know it can do more formatting } else { SpannableStringBuilder spannable = new SpannableStringBuilder(Html.fromHtml( @@ -1136,7 +1140,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable spannableBody = (SpannableStringBuilder) spannable.subSequence(0, i+1); } - if (getInReplyTo() != null && getModerated() == null) { + if (includeReplyTo && getInReplyTo() != null && getModerated() == null) { // Don't show quote if it's the message right before us if (prev() != null && prev().getUuid().equals(getInReplyTo().getUuid())) return spannableBody; diff --git a/src/main/java/eu/siacs/conversations/entities/Reaction.java b/src/main/java/eu/siacs/conversations/entities/Reaction.java index c1ce186481754da43cdf4d8dcb13fd40870fe399..8df66035c65b7d641f757ba94dc7ab94ec0ef98e 100644 --- a/src/main/java/eu/siacs/conversations/entities/Reaction.java +++ b/src/main/java/eu/siacs/conversations/entities/Reaction.java @@ -2,6 +2,9 @@ package eu.siacs.conversations.entities; import androidx.annotation.NonNull; +import com.cheogram.android.EmojiSearch; +import com.cheogram.android.GetThumbnailForCid; + import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.collect.Collections2; @@ -19,6 +22,8 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; +import io.ipfs.cid.Cid; + import eu.siacs.conversations.xmpp.Jid; import java.io.IOException; @@ -30,6 +35,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; public class Reaction { @@ -45,7 +51,7 @@ public class Reaction { private static final Gson GSON; static { - GSON = new GsonBuilder().registerTypeAdapter(Jid.class, new JidTypeAdapter()).create(); + GSON = new GsonBuilder().registerTypeAdapter(Jid.class, new JidTypeAdapter()).registerTypeAdapter(Cid.class, new CidTypeAdapter()).create(); } public final String reaction; @@ -53,14 +59,17 @@ public class Reaction { public final Jid from; public final Jid trueJid; public final String occupantId; + public final Cid cid; public Reaction( final String reaction, + final Cid cid, boolean received, final Jid from, final Jid trueJid, final String occupantId) { this.reaction = reaction; + this.cid = cid; this.received = received; this.from = from; this.trueJid = trueJid; @@ -93,7 +102,7 @@ public class Reaction { builder.addAll(existing); builder.addAll( Collections2.transform( - reactions, r -> new Reaction(r, received, from, trueJid, occupantId))); + reactions, r -> new Reaction(r, null, received, from, trueJid, occupantId))); return builder.build(); } @@ -108,7 +117,7 @@ public class Reaction { builder.addAll(Collections2.filter(existing, e -> !occupantId.equals(e.occupantId))); builder.addAll( Collections2.transform( - reactions, r -> new Reaction(r, received, from, trueJid, occupantId))); + reactions, r -> new Reaction(r, null, received, from, trueJid, occupantId))); return builder.build(); } @@ -116,7 +125,8 @@ public class Reaction { @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("reaction", reaction) + .add("reaction", cid == null ? reaction : null) + .add("cid", cid) .add("received", received) .add("from", from) .add("trueJid", trueJid) @@ -144,7 +154,7 @@ public class Reaction { Collections2.filter(existing, e -> !from.asBareJid().equals(e.from.asBareJid()))); builder.addAll( Collections2.transform( - reactions, r -> new Reaction(r, received, from, null, null))); + reactions, r -> new Reaction(r, null, received, from, null, null))); return builder.build(); } @@ -171,31 +181,58 @@ public class Reaction { } } + private static class CidTypeAdapter extends TypeAdapter { + @Override + public void write(final JsonWriter out, final Cid value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.toString()); + } + } + + @Override + public Cid read(final JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } else if (in.peek() == JsonToken.STRING) { + final String value = in.nextString(); + return Cid.decode(value); + } + throw new IOException("Unexpected token"); + } + } + public static Aggregated aggregated(final Collection reactions) { - final Map aggregatedReactions = + return aggregated(reactions, (r) -> null); + } + + public static Aggregated aggregated(final Collection reactions, Function thumbnailer) { + final Map aggregatedReactions = Maps.transformValues( - Multimaps.index(reactions, r -> r.reaction).asMap(), Collection::size); - final List> sortedList = + Multimaps.index(reactions, r -> r.cid == null ? new EmojiSearch.Emoji(r.reaction, 0) : new EmojiSearch.CustomEmoji(r.reaction, r.cid.toString(), thumbnailer.apply(r).getThumbnail(r.cid), null)).asMap(), Collection::size); + final List> sortedList = Ordering.from( Comparator.comparingInt( - (Map.Entry o) -> o.getValue())) + (Map.Entry o) -> o.getValue())) .reverse() .immutableSortedCopy(aggregatedReactions.entrySet()); return new Aggregated( sortedList, ImmutableSet.copyOf( Collections2.transform( - Collections2.filter(reactions, r -> !r.received), + Collections2.filter(reactions, r -> r.cid == null && !r.received), r -> r.reaction))); } public static final class Aggregated { - public final List> reactions; + public final List> reactions; public final Set ourReactions; private Aggregated( - final List> reactions, Set ourReactions) { + final List> reactions, Set ourReactions) { this.reactions = reactions; this.ourReactions = ourReactions; } diff --git a/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java b/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java index 502c2d1a79602f9c7680269effbbf6d8c330897b..9ed6a8c5b05a0276adaa89f2a39716ded38546a1 100644 --- a/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java +++ b/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java @@ -4,6 +4,8 @@ import android.view.View; import android.view.ViewGroup; import android.util.TypedValue; +import com.cheogram.android.EmojiSearch; + import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; import com.google.android.material.color.MaterialColors; @@ -15,7 +17,6 @@ import eu.siacs.conversations.entities.Reaction; import java.util.Collection; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.function.Consumer; @@ -25,15 +26,16 @@ public class BindingAdapters { final ChipGroup chipGroup, final Reaction.Aggregated reactions, final Consumer> onModifiedReactions, + final Consumer onCustomReaction, final Runnable addReaction) { - setReactions(chipGroup, reactions, true, onModifiedReactions, addReaction); + setReactions(chipGroup, reactions, true, onModifiedReactions, onCustomReaction, addReaction); } public static void setReactionsOnSent( final ChipGroup chipGroup, final Reaction.Aggregated reactions, final Consumer> onModifiedReactions) { - setReactions(chipGroup, reactions, false, onModifiedReactions, null); + setReactions(chipGroup, reactions, false, onModifiedReactions, null, null); } private static void setReactions( @@ -41,34 +43,29 @@ public class BindingAdapters { final Reaction.Aggregated aggregated, final boolean onReceived, final Consumer> onModifiedReactions, + final Consumer onCustomReaction, final Runnable addReaction) { final var context = chipGroup.getContext(); final var size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 35, context.getResources().getDisplayMetrics()); final var corner = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 35, context.getResources().getDisplayMetrics()); final var layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, size); - final List> reactions = aggregated.reactions; + final List> reactions = aggregated.reactions; if (reactions == null || reactions.isEmpty()) { chipGroup.setVisibility(View.GONE); } else { chipGroup.removeAllViews(); chipGroup.setVisibility(View.VISIBLE); - for (final Map.Entry reaction : reactions) { + for (final var reaction : reactions) { final var emoji = reaction.getKey(); final var count = reaction.getValue(); final Chip chip = new Chip(chipGroup.getContext()); //chip.setEnsureMinTouchTargetSize(false); chip.setChipMinHeight(size-32.0f); chip.ensureAccessibleTouchTarget(size); - chip.setChipStartPadding(0.0f); - chip.setChipEndPadding(0.0f); - chip.setChipCornerRadius(corner); chip.setLayoutParams(layoutParams); - if (count == 1) { - chip.setText(emoji); - } else { - chip.setText(String.format(Locale.ENGLISH, "%s %d", emoji, count)); - } - final boolean oneOfOurs = aggregated.ourReactions.contains(emoji); + chip.setChipCornerRadius(corner); + emoji.setupChip(chip, count); + final boolean oneOfOurs = aggregated.ourReactions.contains(emoji.toString()); // received = surface; sent = surface high matches bubbles if (oneOfOurs) { chip.setChipBackgroundColor( @@ -82,6 +79,8 @@ public class BindingAdapters { context, com.google.android.material.R.attr.colorSurfaceContainerLow)); } + chip.setTextEndPadding(0.0f); + chip.setTextStartPadding(0.0f); chip.setOnClickListener( v -> { if (oneOfOurs) { @@ -89,13 +88,17 @@ public class BindingAdapters { ImmutableSet.copyOf( Collections2.filter( aggregated.ourReactions, - r -> !r.equals(emoji)))); + r -> !r.equals(emoji.toString())))); } else { - onModifiedReactions.accept( + if (emoji instanceof EmojiSearch.CustomEmoji) { + onCustomReaction.accept((EmojiSearch.CustomEmoji) emoji); + } else { + onModifiedReactions.accept( new ImmutableSet.Builder() .addAll(aggregated.ourReactions) - .add(emoji) + .add(emoji.toString()) .build()); + } } }); chipGroup.addView(chip); @@ -113,11 +116,11 @@ public class BindingAdapters { // com.google.android.material.R.attr.colorTertiary)); chip.setChipBackgroundColor( MaterialColors.getColorStateListOrNull( - chipGroup.getContext(), + context, com.google.android.material.R.attr.colorSurfaceContainerLow)); chip.setChipIconTint( MaterialColors.getColorStateListOrNull( - chipGroup.getContext(), + context, com.google.android.material.R.attr.colorOnSurface)); //chip.setEnsureMinTouchTargetSize(false); chip.setTextEndPadding(0.0f); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 9345c9051579da686bd6cfd87f7a4a38a0d06deb..607f6149811dd1e2817558c7baf5f019aa9f9ef2 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -60,6 +60,8 @@ import com.google.android.material.shape.CornerFamily; import com.google.android.material.shape.ShapeAppearanceModel; import com.cheogram.android.BobTransfer; +import com.cheogram.android.EmojiSearch; +import com.cheogram.android.GetThumbnailForCid; import com.cheogram.android.MessageTextActionModeCallback; import com.cheogram.android.SwipeDetector; import com.cheogram.android.Util; @@ -90,6 +92,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Locale; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -111,6 +114,7 @@ import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message.FileParams; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.Reaction; import eu.siacs.conversations.entities.Roster; import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.Transferable; @@ -592,27 +596,7 @@ public class MessageAdapter extends ArrayAdapter { private SpannableStringBuilder getSpannableBody(final Message message) { Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), R.drawable.ic_photo_24dp, null); - return message.getMergedBody((cid) -> { - try { - DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid); - if (f == null || !f.canRead()) { - if (!message.trusted() && !message.getConversation().canInferPresence()) return null; - - try { - new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start(); - } catch (final NoSuchAlgorithmException | URISyntaxException e) { } - return null; - } - - Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true); - if (d == null) { - new ThumbnailTask().execute(f); - } - return d; - } catch (final IOException e) { - return null; - } - }, fallbackImg); + return message.getMergedBody(new Thumbnailer(message), fallbackImg); } private void displayTextMessage( @@ -1499,6 +1483,7 @@ public class MessageAdapter extends ArrayAdapter { setTextColor(viewHolder.messageBody, bubbleColor); viewHolder.messageBody.setLinkTextColor(bubbleToOnSurfaceColor(viewHolder.messageBody, bubbleColor)); + final Function reactionThumbnailer = (r) -> new Thumbnailer(conversation.getAccount(), r, conversation.canInferPresence()); if (type == RECEIVED) { if (!muted && commands != null && conversation instanceof Conversation) { CommandButtonAdapter adapter = new CommandButtonAdapter(activity); @@ -1532,16 +1517,20 @@ public class MessageAdapter extends ArrayAdapter { CryptoHelper.encryptionTypeToText(message.getEncryption())); } } + final var aggregatedReactions = conversation instanceof Conversation ? ((Conversation) conversation).aggregatedReactionsFor(message, reactionThumbnailer) : message.getAggregatedReactions(); BindingAdapters.setReactionsOnReceived( viewHolder.reactions, - conversation instanceof Conversation ? ((Conversation) conversation).aggregatedReactionsFor(message) : message.getAggregatedReactions(), + aggregatedReactions, reactions -> sendReactions(message, reactions), + emoji -> sendCustomReaction(message, emoji), () -> addReaction(message)); } else if (type == SENT) { + final var aggregatedReactions = conversation instanceof Conversation ? ((Conversation) conversation).aggregatedReactionsFor(message, reactionThumbnailer) : message.getAggregatedReactions(); BindingAdapters.setReactionsOnReceived( viewHolder.reactions, - conversation instanceof Conversation ? ((Conversation) conversation).aggregatedReactionsFor(message) : message.getAggregatedReactions(), + aggregatedReactions, reactions -> sendReactions(message, reactions), + emoji -> sendCustomReaction(message, emoji), () -> addReaction(message)); } @@ -1617,6 +1606,13 @@ public class MessageAdapter extends ArrayAdapter { Toast.makeText(activity, R.string.could_not_add_reaction, Toast.LENGTH_LONG).show(); } + private void sendCustomReaction(final Message inReplyTo, final EmojiSearch.CustomEmoji emoji) { + final var message = inReplyTo.reply(); + message.appendBody(emoji.toInsert()); + Message.configurePrivateMessage(message); + new Thread(() -> activity.xmppConnectionService.sendMessage(message)).start(); + } + private void addReaction(final Message message) { activity.addReaction(message, reactions -> activity.xmppConnectionService.sendReactions(message,reactions)); } @@ -1831,6 +1827,47 @@ public class MessageAdapter extends ArrayAdapter { protected ChipGroup reactions; } + class Thumbnailer implements GetThumbnailForCid { + final Account account; + final boolean canFetch; + final Jid counterpart; + + public Thumbnailer(final Message message) { + account = message.getConversation().getAccount(); + canFetch = message.trusted() || message.getConversation().canInferPresence(); + counterpart = message.getCounterpart(); + } + + public Thumbnailer(final Account account, final Reaction reaction, final boolean allowFetch) { + canFetch = allowFetch; + counterpart = reaction.from; + this.account = account; + } + + @Override + public Drawable getThumbnail(Cid cid) { + try { + DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid); + if (f == null || !f.canRead()) { + if (!canFetch) return null; + + try { + new BobTransfer(BobTransfer.uri(cid), account, counterpart, activity.xmppConnectionService).start(); + } catch (final NoSuchAlgorithmException | URISyntaxException e) { } + return null; + } + + Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true); + if (d == null) { + new ThumbnailTask().execute(f); + } + return d; + } catch (final IOException e) { + return null; + } + } + } + class ThumbnailTask extends AsyncTask { @Override protected Drawable[] doInBackground(DownloadableFile... params) {