Show custom emoji reactions and allow sending them
(cherry picked from commit 7a51666a8bfe56bae3299dc386cd778236143874)
This commit is contained in:
parent
293c21db2c
commit
699eb7eae9
6 changed files with 317 additions and 141 deletions
|
@ -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;
|
||||
|
@ -67,6 +68,8 @@ import com.caverock.androidsvg.SVG;
|
|||
import de.monocles.chat.ConversationPage;
|
||||
import de.monocles.chat.Util;
|
||||
import de.monocles.chat.WebxdcPage;
|
||||
import de.monocles.chat.BobTransfer;
|
||||
import de.monocles.chat.GetThumbnailForCid;
|
||||
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
@ -108,6 +111,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;
|
||||
|
||||
|
@ -309,6 +313,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 {
|
||||
|
@ -324,6 +329,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) {
|
||||
|
@ -735,26 +741,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()));
|
||||
|
@ -764,7 +803,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) {
|
||||
|
@ -934,15 +973,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1534,6 +1575,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()) {
|
||||
|
@ -1553,6 +1595,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;
|
||||
}
|
||||
|
@ -2454,7 +2497,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
float screenWidth = binding.getRoot().getContext().getResources().getDisplayMetrics().widthPixels;
|
||||
TextPaint paint = ((TextView) LayoutInflater.from(binding.getRoot().getContext()).inflate(R.layout.radio_grid_item, null)).getPaint();
|
||||
float maxColumnWidth = theOptions.stream().map((x) ->
|
||||
StaticLayout.getDesiredWidth(x.toString(), paint)
|
||||
StaticLayout.getDesiredWidth(x.toString(), paint)
|
||||
).max(Float::compare).orElse(new Float(0.0));
|
||||
if (maxColumnWidth * theOptions.size() < 0.90 * screenWidth) {
|
||||
binding.radios.setNumColumns(theOptions.size());
|
||||
|
@ -2547,22 +2590,22 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
|
||||
final SVG icon = getItem(position).getIcon();
|
||||
if (icon != null) {
|
||||
final Element iconEl = getItem(position).getIconEl();
|
||||
if (height < 1) {
|
||||
v.measure(0, 0);
|
||||
height = v.getMeasuredHeight();
|
||||
}
|
||||
if (height < 1) return v;
|
||||
if (mediaSelector) {
|
||||
final Drawable d = getDrawableForSVG(icon, iconEl, height * 4);
|
||||
if (d != null) {
|
||||
final int boundsHeight = 35 + (int)((height * 4) / xmppConnectionService.getResources().getDisplayMetrics().density);
|
||||
d.setBounds(0, 0, d.getIntrinsicWidth(), boundsHeight);
|
||||
}
|
||||
v.setCompoundDrawables(null, d, null, null);
|
||||
} else {
|
||||
v.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawableForSVG(icon, iconEl, height), null, null, null);
|
||||
}
|
||||
final Element iconEl = getItem(position).getIconEl();
|
||||
if (height < 1) {
|
||||
v.measure(0, 0);
|
||||
height = v.getMeasuredHeight();
|
||||
}
|
||||
if (height < 1) return v;
|
||||
if (mediaSelector) {
|
||||
final Drawable d = getDrawableForSVG(icon, iconEl, height * 4);
|
||||
if (d != null) {
|
||||
final int boundsHeight = 35 + (int)((height * 4) / xmppConnectionService.getResources().getDisplayMetrics().density);
|
||||
d.setBounds(0, 0, d.getIntrinsicWidth(), boundsHeight);
|
||||
}
|
||||
v.setCompoundDrawables(null, d, null, null);
|
||||
} else {
|
||||
v.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawableForSVG(icon, iconEl, height), null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
|
@ -2650,9 +2693,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
|
||||
final SVG defaultIcon = defaultOption.getIcon();
|
||||
if (defaultIcon != null) {
|
||||
DisplayMetrics display = mPager.getContext().getResources().getDisplayMetrics();
|
||||
int height = (int)(display.heightPixels*display.density/4);
|
||||
binding.defaultButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, getDrawableForSVG(defaultIcon, defaultOption.getIconEl(), height), null, null);
|
||||
DisplayMetrics display = mPager.getContext().getResources().getDisplayMetrics();
|
||||
int height = (int)(display.heightPixels*display.density/4);
|
||||
binding.defaultButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, getDrawableForSVG(defaultIcon, defaultOption.getIconEl(), height), null, null);
|
||||
}
|
||||
|
||||
binding.defaultButton.setText(defaultOption.toString());
|
||||
|
@ -2962,10 +3005,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
viewType = TYPE_CHECKBOX_FIELD;
|
||||
}
|
||||
} else if (
|
||||
range != null && range.getAttribute("min") != null && range.getAttribute("max") != null && (
|
||||
"xs:integer".equals(datatype) || "xs:int".equals(datatype) || "xs:long".equals(datatype) || "xs:short".equals(datatype) || "xs:byte".equals(datatype) ||
|
||||
"xs:decimal".equals(datatype) || "xs:double".equals(datatype)
|
||||
)
|
||||
range != null && range.getAttribute("min") != null && range.getAttribute("max") != null && (
|
||||
"xs:integer".equals(datatype) || "xs:int".equals(datatype) || "xs:long".equals(datatype) || "xs:short".equals(datatype) || "xs:byte".equals(datatype) ||
|
||||
"xs:decimal".equals(datatype) || "xs:double".equals(datatype)
|
||||
)
|
||||
) {
|
||||
// has a range and is numeric, use a slider
|
||||
viewType = TYPE_SLIDER_FIELD;
|
||||
|
@ -3196,8 +3239,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
eu.siacs.conversations.xmpp.forms.Field fillableField = null;
|
||||
for (eu.siacs.conversations.xmpp.forms.Field field : form.getFields()) {
|
||||
if ((field.getType() == null || (!field.getType().equals("hidden") && !field.getType().equals("fixed"))) && field.getFieldName() != null && !field.getFieldName().equals("http://jabber.org/protocol/commands#actions")) {
|
||||
final var validate = field.findChild("validate", "http://jabber.org/protocol/xdata-validate");
|
||||
final var range = validate == null ? null : validate.findChild("range", "http://jabber.org/protocol/xdata-validate");
|
||||
final var validate = field.findChild("validate", "http://jabber.org/protocol/xdata-validate");
|
||||
final var range = validate == null ? null : validate.findChild("range", "http://jabber.org/protocol/xdata-validate");
|
||||
fillableField = range == null ? field : null;
|
||||
fillableFieldCount++;
|
||||
}
|
||||
|
@ -3286,8 +3329,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
Data dataForm = null;
|
||||
if (responseElement != null && responseElement.getName().equals("x") && responseElement.getNamespace().equals("jabber:x:data")) dataForm = Data.parse(responseElement);
|
||||
if (mNode.equals("jabber:iq:register") &&
|
||||
xmppConnectionService.getPreferences().contains("onboarding_action") &&
|
||||
dataForm != null && dataForm.getFieldByName("gateway-jid") != null) {
|
||||
xmppConnectionService.getPreferences().contains("onboarding_action") &&
|
||||
dataForm != null && dataForm.getFieldByName("gateway-jid") != null) {
|
||||
|
||||
|
||||
dataForm.put("gateway-jid", xmppConnectionService.getPreferences().getString("onboarding_action", ""));
|
||||
|
@ -3376,8 +3419,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
if (el.getName().equals("item")) {
|
||||
for (Element subel : el.getChildren()) {
|
||||
if (subel.getAttribute("var").equals(reportedField.getVar())) {
|
||||
itemField = subel;
|
||||
break;
|
||||
itemField = subel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3538,11 +3581,11 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
|
||||
String formType = responseElement == null ? null : responseElement.getAttribute("type");
|
||||
if (!action.equals("cancel") &&
|
||||
!action.equals("prev") &&
|
||||
responseElement != null &&
|
||||
responseElement.getName().equals("x") &&
|
||||
responseElement.getNamespace().equals("jabber:x:data") &&
|
||||
formType != null && formType.equals("form")) {
|
||||
!action.equals("prev") &&
|
||||
responseElement != null &&
|
||||
responseElement.getName().equals("x") &&
|
||||
responseElement.getNamespace().equals("jabber:x:data") &&
|
||||
formType != null && formType.equals("form")) {
|
||||
|
||||
Data form = Data.parse(responseElement);
|
||||
eu.siacs.conversations.xmpp.forms.Field actionList = form.getFieldByName("http://jabber.org/protocol/commands#actions");
|
||||
|
@ -3621,9 +3664,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
float screenWidth = ctx.getResources().getDisplayMetrics().widthPixels;
|
||||
TextPaint paint = ((TextView) LayoutInflater.from(mPager.getContext()).inflate(R.layout.command_result_cell, null)).getPaint();
|
||||
float tableHeaderWidth = reported.stream().reduce(
|
||||
0f,
|
||||
(total, field) -> total + StaticLayout.getDesiredWidth(field.getLabel().or("--------") + "\t", paint),
|
||||
(a, b) -> a + b
|
||||
0f,
|
||||
(total, field) -> total + StaticLayout.getDesiredWidth(field.getLabel().or("--------") + "\t", paint),
|
||||
(a, b) -> a + b
|
||||
);
|
||||
|
||||
spanCount = tableHeaderWidth > 0.59 * screenWidth ? 1 : this.reported.size();
|
||||
|
@ -3715,11 +3758,11 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
}
|
||||
|
||||
private Drawable getDrawableForSVG(SVG svg, Element svgElement, int size) {
|
||||
if (svgElement != null && svgElement.getChildren().size() == 1 && svgElement.getChildren().get(0).getName().equals("image")) {
|
||||
return getDrawableForUrl(svgElement.getChildren().get(0).getAttribute("href"));
|
||||
} else {
|
||||
return xmppConnectionService.getFileBackend().drawSVG(svg, size);
|
||||
}
|
||||
if (svgElement != null && svgElement.getChildren().size() == 1 && svgElement.getChildren().get(0).getName().equals("image")) {
|
||||
return getDrawableForUrl(svgElement.getChildren().get(0).getAttribute("href"));
|
||||
} else {
|
||||
return xmppConnectionService.getFileBackend().drawSVG(svg, size);
|
||||
}
|
||||
}
|
||||
|
||||
private Drawable getDrawableForUrl(final String url) {
|
||||
|
@ -3835,8 +3878,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
final var packet = new Iq(Iq.Type.SET);
|
||||
packet.setTo(response.getFrom());
|
||||
final Element form = packet
|
||||
.addChild("query", "http://jabber.org/protocol/muc#owner")
|
||||
.addChild("x", "jabber:x:data");
|
||||
.addChild("query", "http://jabber.org/protocol/muc#owner")
|
||||
.addChild("x", "jabber:x:data");
|
||||
form.setAttribute("type", "cancel");
|
||||
xmppConnectionService.sendIqPacket(getAccount(), packet, null);
|
||||
return true;
|
||||
|
@ -3849,14 +3892,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
|
||||
String formType = responseElement == null ? null : responseElement.getAttribute("type");
|
||||
if (responseElement != null &&
|
||||
responseElement.getName().equals("x") &&
|
||||
responseElement.getNamespace().equals("jabber:x:data") &&
|
||||
formType != null && formType.equals("form")) {
|
||||
responseElement.getName().equals("x") &&
|
||||
responseElement.getNamespace().equals("jabber:x:data") &&
|
||||
formType != null && formType.equals("form")) {
|
||||
|
||||
responseElement.setAttribute("type", "submit");
|
||||
packet
|
||||
.addChild("query", "http://jabber.org/protocol/muc#owner")
|
||||
.addChild(responseElement);
|
||||
.addChild("query", "http://jabber.org/protocol/muc#owner")
|
||||
.addChild(responseElement);
|
||||
}
|
||||
|
||||
executing = true;
|
||||
|
|
|
@ -1091,10 +1091,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(
|
||||
|
@ -1143,7 +1147,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 de.monocles.chat.EmojiSearch;
|
||||
import de.monocles.chat.GetThumbnailForCid;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Collections2;
|
||||
|
@ -18,6 +21,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;
|
||||
|
@ -28,6 +33,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 {
|
||||
|
||||
|
@ -43,7 +49,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;
|
||||
|
@ -51,14 +57,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;
|
||||
|
@ -91,7 +100,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();
|
||||
}
|
||||
|
||||
|
@ -106,7 +115,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();
|
||||
}
|
||||
|
||||
|
@ -114,7 +123,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)
|
||||
|
@ -142,7 +152,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();
|
||||
}
|
||||
|
||||
|
@ -169,31 +179,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 de.monocles.chat.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);
|
||||
|
|
|
@ -67,6 +67,17 @@ import de.monocles.chat.MessageTextActionModeCallback;
|
|||
import de.monocles.chat.Util;
|
||||
import de.monocles.chat.WebxdcPage;
|
||||
import de.monocles.chat.WebxdcUpdate;
|
||||
import de.monocles.chat.BobTransfer;
|
||||
import de.monocles.chat.EmojiSearch;
|
||||
import de.monocles.chat.GetThumbnailForCid;
|
||||
import de.monocles.chat.MessageTextActionModeCallback;
|
||||
import de.monocles.chat.SwipeDetector;
|
||||
import de.monocles.chat.Util;
|
||||
import de.monocles.chat.WebxdcPage;
|
||||
import de.monocles.chat.WebxdcUpdate;
|
||||
|
||||
import androidx.emoji2.emojipicker.EmojiViewItem;
|
||||
import androidx.emoji2.emojipicker.RecentEmojiProvider;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
|
@ -91,6 +102,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -112,6 +124,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;
|
||||
|
@ -296,9 +309,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
fileSize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
|
||||
if (message.getStatus() == Message.STATUS_SEND_FAILED
|
||||
|| (transferable != null
|
||||
&& (transferable.getStatus() == Transferable.STATUS_FAILED
|
||||
|| transferable.getStatus()
|
||||
== Transferable.STATUS_CANCELLED))) {
|
||||
&& (transferable.getStatus() == Transferable.STATUS_FAILED
|
||||
|| transferable.getStatus()
|
||||
== Transferable.STATUS_CANCELLED))) {
|
||||
error = true;
|
||||
} else {
|
||||
error = message.getStatus() == Message.STATUS_SEND_FAILED;
|
||||
|
@ -498,10 +511,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
if (makeEdits && end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
|
||||
body.insert(end, "\n");
|
||||
body.setSpan(
|
||||
new DividerSpan(false),
|
||||
end,
|
||||
end + ("\n".equals(body.subSequence(end + 1, end + 2).toString()) ? 2 : 1),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
new DividerSpan(false),
|
||||
end,
|
||||
end + ("\n".equals(body.subSequence(end + 1, end + 2).toString()) ? 2 : 1),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
|
||||
|
@ -593,27 +606,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(
|
||||
|
@ -903,8 +896,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
if (lastUpdate != null && (lastUpdate.getSummary() != null || lastUpdate.getDocument() != null)) {
|
||||
viewHolder.messageBody.setVisibility(View.VISIBLE);
|
||||
viewHolder.messageBody.setText(
|
||||
(lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") +
|
||||
(lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary())
|
||||
(lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") +
|
||||
(lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1447,7 +1440,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
|
||||
MessageAdapter.this.mOnMessageBoxClickedListener
|
||||
.onContactPictureClicked(message);
|
||||
.onContactPictureClicked(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1583,6 +1576,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);
|
||||
|
@ -1616,16 +1610,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));
|
||||
}
|
||||
|
||||
|
@ -1701,6 +1699,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));
|
||||
}
|
||||
|
@ -1728,8 +1733,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
public void openDownloadable(Message message) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
&& ContextCompat.checkSelfPermission(
|
||||
activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ConversationFragment.registerPendingMessage(activity, message);
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
|
@ -1915,6 +1920,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) {
|
||||
|
|
|
@ -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> {
|
||||
|
|
Loading…
Reference in a new issue