Detailed changes
@@ -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<Emoji, EmojiSearchAdapter.ViewHolder> {
@@ -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<String, Reaction> 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<Reaction, GetThumbnailForCid> thumbnailer) {
Set<Reaction> 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;
}
@@ -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;
@@ -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<Cid> {
+ @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<Reaction> reactions) {
- final Map<String, Integer> aggregatedReactions =
+ return aggregated(reactions, (r) -> null);
+ }
+
+ public static Aggregated aggregated(final Collection<Reaction> reactions, Function<Reaction, GetThumbnailForCid> thumbnailer) {
+ final Map<EmojiSearch.Emoji, Integer> aggregatedReactions =
Maps.transformValues(
- Multimaps.index(reactions, r -> r.reaction).asMap(), Collection::size);
- final List<Map.Entry<String, Integer>> 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<Map.Entry<EmojiSearch.Emoji, Integer>> sortedList =
Ordering.from(
Comparator.comparingInt(
- (Map.Entry<String, Integer> o) -> o.getValue()))
+ (Map.Entry<EmojiSearch.Emoji, Integer> 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<Map.Entry<String, Integer>> reactions;
+ public final List<Map.Entry<EmojiSearch.Emoji, Integer>> reactions;
public final Set<String> ourReactions;
private Aggregated(
- final List<Map.Entry<String, Integer>> reactions, Set<String> ourReactions) {
+ final List<Map.Entry<EmojiSearch.Emoji, Integer>> reactions, Set<String> ourReactions) {
this.reactions = reactions;
this.ourReactions = ourReactions;
}
@@ -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<Collection<String>> onModifiedReactions,
+ final Consumer<EmojiSearch.CustomEmoji> 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<Collection<String>> 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<Collection<String>> onModifiedReactions,
+ final Consumer<EmojiSearch.CustomEmoji> 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<Map.Entry<String, Integer>> reactions = aggregated.reactions;
+ final List<Map.Entry<EmojiSearch.Emoji, Integer>> reactions = aggregated.reactions;
if (reactions == null || reactions.isEmpty()) {
chipGroup.setVisibility(View.GONE);
} else {
chipGroup.removeAllViews();
chipGroup.setVisibility(View.VISIBLE);
- for (final Map.Entry<String, Integer> 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<String>()
.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);
@@ -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<Message> {
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<Message> {
setTextColor(viewHolder.messageBody, bubbleColor);
viewHolder.messageBody.setLinkTextColor(bubbleToOnSurfaceColor(viewHolder.messageBody, bubbleColor));
+ final Function<Reaction, GetThumbnailForCid> 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<Message> {
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<Message> {
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<Message> {
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<DownloadableFile, Void, Drawable[]> {
@Override
protected Drawable[] doInBackground(DownloadableFile... params) {