Detailed changes
@@ -2,6 +2,8 @@ package com.cheogram.android;
import android.util.Log;
import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.view.LayoutInflater;
import android.view.View;
@@ -48,7 +50,11 @@ public class EmojiSearch {
}
}
- public List<Emoji> find(final String q) {
+ public synchronized void addEmoji(final Emoji one) {
+ emoji.add(one);
+ }
+
+ public synchronized List<Emoji> find(final String q) {
final Set<Emoji> emoticon = new TreeSet<>();
for (Emoji e : emoji) {
if (e.emoticonMatch(q)) {
@@ -75,6 +81,17 @@ public class EmojiSearch {
List<Emoji> lst = new ArrayList<>(emoticon);
lst.addAll(Lists.transform(result, (r) -> r.getReferent()));
+
+ List<Emoji> scanList = new ArrayList<>(lst);
+ int inserted = 0;
+ for (int i = 0; i < scanList.size(); i++) {
+ for (Emoji e : emoji) {
+ if (e.shortcodeMatch(scanList.get(i).uniquePart())) {
+ inserted ++;
+ lst.add(i + inserted, e);
+ }
+ }
+ }
return lst;
}
@@ -90,6 +107,12 @@ public class EmojiSearch {
protected final List<String> shortcodes = new ArrayList<>();
protected final String fuzzyFind;
+ public Emoji(final String unicode, final int order, final String fuzzyFind) {
+ this.unicode = unicode;
+ this.order = order;
+ this.fuzzyFind = fuzzyFind;
+ }
+
public Emoji(JSONObject o) throws JSONException {
unicode = o.getString("unicode");
order = o.getInt("order");
@@ -116,18 +139,60 @@ public class EmojiSearch {
return false;
}
+ public boolean shortcodeMatch(final String q) {
+ for (final String shortcode : shortcodes) {
+ if (shortcode.equals(q)) return true;
+ }
+
+ return false;
+ }
+
public SpannableStringBuilder toInsert() {
return new SpannableStringBuilder(unicode);
}
+ public String uniquePart() {
+ return unicode;
+ }
+
+ @Override
public int compareTo(Emoji o) {
if (equals(o)) return 0;
- if (order == o.order) return -1;
+ if (order == o.order) return uniquePart().compareTo(o.uniquePart());
return order - o.order;
}
- public boolean equals(Emoji o) {
- return toInsert().equals(o.toInsert());
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Emoji)) return false;
+
+ return uniquePart().equals(((Emoji) o).uniquePart());
+ }
+ }
+
+ public static class CustomEmoji extends Emoji {
+ protected final String source;
+ protected final Drawable icon;
+
+ public CustomEmoji(final String shortcode, final String source, final Drawable icon) {
+ super(null, 10, shortcode);
+ shortcodes.add(shortcode);
+ 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);
+ return builder;
+ }
+
+ @Override
+ public String uniquePart() {
+ return source;
}
}
@@ -139,7 +204,15 @@ public class EmojiSearch {
@Override
public View getView(int position, View view, ViewGroup parent) {
EmojiSearchRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.emoji_search_row, parent, false);
- binding.unicode.setText(getItem(position).toInsert());
+ if (getItem(position) instanceof CustomEmoji) {
+ binding.nonunicode.setText(getItem(position).toInsert());
+ binding.nonunicode.setVisibility(View.VISIBLE);
+ binding.unicode.setVisibility(View.GONE);
+ } else {
+ binding.unicode.setText(getItem(position).toInsert());
+ binding.unicode.setVisibility(View.VISIBLE);
+ binding.nonunicode.setVisibility(View.GONE);
+ }
binding.shortcode.setText(getItem(position).shortcodes.get(0));
return binding.getRoot();
}
@@ -0,0 +1,39 @@
+package com.cheogram.android;
+
+import android.graphics.Paint;
+import android.graphics.drawable.AnimatedImageDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.style.ImageSpan;
+
+public class InlineImageSpan extends ImageSpan {
+ private final Paint.FontMetricsInt mTmpFontMetrics = new Paint.FontMetricsInt();
+ private final float dHeight;
+ private final float dWidth;
+
+ public InlineImageSpan(Drawable d, final String source) {
+ super(d.getConstantState() == null ? d : d.getConstantState().newDrawable(), source);
+ dHeight = d.getIntrinsicHeight();
+ dWidth = d.getIntrinsicWidth();
+ if (Build.VERSION.SDK_INT >= 28 && d instanceof AnimatedImageDrawable) {
+ ((AnimatedImageDrawable) getDrawable()).start();
+ }
+ }
+
+ @Override
+ public int getSize(final Paint paint, final CharSequence text, final int start, final int end, final Paint.FontMetricsInt fm) {
+ paint.getFontMetricsInt(mTmpFontMetrics);
+ final int fontHeight = Math.abs(mTmpFontMetrics.descent - mTmpFontMetrics.ascent);
+ float mRatio = fontHeight * 1.0f / dHeight;
+ int mHeight = (short) (dHeight * mRatio);
+ int mWidth = (short) (dWidth * mRatio);
+ getDrawable().setBounds(0, 0, mWidth, mHeight);
+ if (fm != null) {
+ fm.ascent = mTmpFontMetrics.ascent;
+ fm.descent = mTmpFontMetrics.descent;
+ fm.top = mTmpFontMetrics.top;
+ fm.bottom = mTmpFontMetrics.bottom;
+ }
+ return mWidth;
+ }
+}
@@ -0,0 +1,129 @@
+package com.cheogram.android;
+
+import android.app.Application;
+import android.graphics.Typeface;
+import android.text.Spanned;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.AlignmentSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.BulletSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.ParagraphStyle;
+import android.text.style.QuoteSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.SubscriptSpan;
+import android.text.style.SuperscriptSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
+import android.text.style.UnderlineSpan;
+
+import io.ipfs.cid.Cid;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.TextNode;
+
+public class SpannedToXHTML {
+ public static Element append(Element out, Spanned text) {
+ withinParagraph(out, text, 0, text.length());
+ return out;
+ }
+
+ private static void withinParagraph(Element outer, Spanned text, int start, int end) {
+ int next;
+ outer:
+ for (int i = start; i < end; i = next) {
+ Element out = outer;
+ next = text.nextSpanTransition(i, end, CharacterStyle.class);
+ CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);
+ for (int j = 0; j < style.length; j++) {
+ if (style[j] instanceof StyleSpan) {
+ int s = ((StyleSpan) style[j]).getStyle();
+ if ((s & Typeface.BOLD) != 0) {
+ out = out.addChild("b");
+ }
+ if ((s & Typeface.ITALIC) != 0) {
+ out = out.addChild("i");
+ }
+ }
+ if (style[j] instanceof TypefaceSpan) {
+ String s = ((TypefaceSpan) style[j]).getFamily();
+ if ("monospace".equals(s)) {
+ out = out.addChild("tt");
+ }
+ }
+ if (style[j] instanceof SuperscriptSpan) {
+ out = out.addChild("sup");
+ }
+ if (style[j] instanceof SubscriptSpan) {
+ out = out.addChild("sub");
+ }
+ // TextEdit underlines text in current word, which ends up getting sent...
+ // SPAN_COMPOSING ?
+ /*if (style[j] instanceof UnderlineSpan) {
+ out = out.addChild("u");
+ }*/
+ if (style[j] instanceof StrikethroughSpan) {
+ out = out.addChild("span");
+ out.setAttribute("style", "text-decoration:line-through;");
+ }
+ if (style[j] instanceof URLSpan) {
+ out = out.addChild("a");
+ out.setAttribute("href", ((URLSpan) style[j]).getURL());
+ }
+ if (style[j] instanceof ImageSpan) {
+ String source = ((ImageSpan) style[j]).getSource();
+ if (source != null && source.length() > 0 && source.charAt(0) == 'z') {
+ try {
+ source = BobTransfer.uri(Cid.decode(source)).toString();
+ } catch (final Exception e) { }
+ }
+ out = out.addChild("img");
+ out.setAttribute("src", source);
+ out.setAttribute("alt", text.subSequence(i, next).toString());
+ continue outer;
+ }
+ if (style[j] instanceof AbsoluteSizeSpan) {
+ try {
+ AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]);
+ float sizeDip = s.getSize();
+ if (!s.getDip()) {
+ Class activityThreadClass = Class.forName("android.app.ActivityThread");
+ Application application = (Application) activityThreadClass.getMethod("currentApplication").invoke(null);
+ sizeDip /= application.getResources().getDisplayMetrics().density;
+ }
+ // px in CSS is the equivalance of dip in Android
+ out = out.addChild("span");
+ out.setAttribute("style", String.format("font-size:%.0fpx;", sizeDip));
+ } catch (final Exception e) { }
+ }
+ if (style[j] instanceof RelativeSizeSpan) {
+ float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();
+ out = out.addChild("span");
+ out.setAttribute("style", String.format("font-size:%.0fem;", sizeEm));
+ }
+ if (style[j] instanceof ForegroundColorSpan) {
+ int color = ((ForegroundColorSpan) style[j]).getForegroundColor();
+ out = out.addChild("span");
+ out.setAttribute("style", String.format("color:#%06X;", 0xFFFFFF & color));
+ }
+ if (style[j] instanceof BackgroundColorSpan) {
+ int color = ((BackgroundColorSpan) style[j]).getBackgroundColor();
+ out = out.addChild("span");
+ out.setAttribute("style", String.format("background-color:#%06X;", 0xFFFFFF & color));
+ }
+ }
+ String content = text.subSequence(i, next).toString();
+ for (int c = 0; c < content.length(); c++) {
+ if (content.charAt(c) == '\n') {
+ out.addChild("br");
+ } else {
+ out.addChild(new TextNode("" + content.charAt(c)));
+ }
+ }
+ }
+ }
+}
@@ -7,11 +7,21 @@
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="horizontal"
- android:paddingTop="8dp"
- android:paddingBottom="8dp"
- android:paddingLeft="@dimen/avatar_item_distance"
- android:paddingRight="@dimen/avatar_item_distance"
- android:background="@drawable/list_choice">
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="@dimen/avatar_item_distance"
+ android:paddingRight="@dimen/avatar_item_distance"
+ android:background="@drawable/list_choice">
+
+ <TextView
+ android:id="@+id/nonunicode"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:textAppearance="@style/TextAppearance.Conversations.Body1"
+ app:emojiCompatEnabled="false"
+ android:visibility="gone"
+ android:textColor="?attr/edit_text_color" />
<TextView
android:id="@+id/unicode"
@@ -25,8 +35,8 @@
android:id="@+id/shortcode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:paddingLeft="8dp"
android:gravity="center_vertical"
+ android:paddingLeft="8dp"
android:textAppearance="@style/TextAppearance.Conversations.Body1"
android:textColor="?attr/edit_text_color" />
@@ -1,4 +1,5 @@
package eu.siacs.conversations.entities;
+import android.util.Log;
import android.content.ContentValues;
import android.database.Cursor;
@@ -17,6 +18,8 @@ import android.view.View;
import com.cheogram.android.BobTransfer;
import com.cheogram.android.GetThumbnailForCid;
+import com.cheogram.android.InlineImageSpan;
+import com.cheogram.android.SpannedToXHTML;
import com.google.common.io.ByteSource;
import com.google.common.base.Strings;
@@ -534,6 +537,26 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.payloads.removeAll(getFallbacks(includeFor));
}
+ public synchronized Element getOrMakeHtml() {
+ Element html = getHtml();
+ if (html != null) return html;
+ html = new Element("html", "http://jabber.org/protocol/xhtml-im");
+ Element body = html.addChild("body", "http://www.w3.org/1999/xhtml");
+ SpannedToXHTML.append(body, new SpannableStringBuilder(getBody()));
+ addPayload(html);
+ return body;
+ }
+
+ public synchronized void setBody(Spanned span) {
+ setBody(span.toString());
+ final Element body = getOrMakeHtml();
+ body.clearChildren();
+ SpannedToXHTML.append(body, span);
+ if (body.getContent().equals(span.toString())) {
+ this.payloads.remove(getHtml(true));
+ }
+ }
+
public synchronized void setBody(String body) {
this.body = body;
this.isGeoUri = null;
@@ -541,6 +564,15 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
this.treatAsDownloadable = null;
}
+ public synchronized void appendBody(Spanned append) {
+ final Element body = getOrMakeHtml();
+ SpannedToXHTML.append(body, append);
+ if (body.getContent().equals(this.body + append.toString())) {
+ this.payloads.remove(getHtml());
+ }
+ appendBody(append.toString());
+ }
+
public synchronized void appendBody(String append) {
this.body += append;
this.isGeoUri = null;
@@ -979,6 +1011,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public void onClick(View widget) { }
};
+ spannable.removeSpan(span);
+ spannable.setSpan(new InlineImageSpan(span.getDrawable(), span.getSource()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(click_span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
@@ -1140,11 +1174,15 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public Element getHtml() {
+ return getHtml(false);
+ }
+
+ public Element getHtml(boolean root) {
if (this.payloads == null) return null;
for (Element el : this.payloads) {
if (el.getName().equals("html") && el.getNamespace().equals("http://jabber.org/protocol/xhtml-im")) {
- return el.getChildren().get(0);
+ return root ? el : el.getChildren().get(0);
}
}
@@ -1187,6 +1225,19 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public synchronized boolean bodyIsOnlyEmojis() {
if (isEmojisOnly == null) {
isEmojisOnly = Emoticons.isOnlyEmoji(getBody().replaceAll("\\s", ""));
+ if (isEmojisOnly) return true;
+
+ if (getHtml() != null) {
+ SpannableStringBuilder spannable = getSpannableBody(null, null);
+ 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);
+ }
+ final String after = spannable.toString().replaceAll("\\s", "");
+ isEmojisOnly = after.length() == 0 || Emoticons.isOnlyEmoji(after);
+ }
}
return isEmojisOnly;
}
@@ -802,6 +802,8 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
public DownloadableFile getFileForCid(Cid cid) {
+ if (cid == null) return null;
+
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query("cheogram.cids", new String[]{"path"}, "cid=?", new String[]{cid.toString()}, null, null, null);
DownloadableFile f = null;
@@ -21,6 +21,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.graphics.Bitmap;
+import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
@@ -40,6 +41,7 @@ import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
+import android.provider.DocumentsContract;
import android.security.KeyChain;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
@@ -62,6 +64,7 @@ import com.cheogram.android.WebxdcUpdate;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
+import com.google.common.io.Files;
import com.kedia.ogparser.OpenGraphCallback;
import com.kedia.ogparser.OpenGraphParser;
@@ -151,6 +154,7 @@ import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.EasyOnboardingInvite;
import eu.siacs.conversations.utils.ExceptionHelper;
+import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.QuickLoader;
@@ -241,8 +245,10 @@ public class XmppConnectionService extends Service {
};
public DatabaseBackend databaseBackend;
private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
+ private final ReplacingSerialSingleThreadExecutor mStickerScanExecutor = new ReplacingSerialSingleThreadExecutor("StickerScan");
private long mLastActivity = 0;
private long mLastMucPing = 0;
+ private long mLastStickerRescan = 0;
private final FileBackend fileBackend = new FileBackend(this);
private MemorizingTrustManager mMemorizingTrustManager;
private final NotificationService mNotificationService = new NotificationService(this);
@@ -720,6 +726,49 @@ public class XmppConnectionService extends Service {
});
}
+ private File stickerDir() {
+ SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
+ final String dir = p.getString("sticker_directory", "Stickers");
+ if (dir.startsWith("content://")) {
+ Uri uri = Uri.parse(dir);
+ uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
+ return new File(FileUtils.getPath(getBaseContext(), uri));
+ } else {
+ return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/" + dir);
+ }
+ }
+
+ public void rescanStickers() {
+ long msToRescan = (mLastStickerRescan + 600000L) - SystemClock.elapsedRealtime();
+ if (msToRescan > 0) return;
+
+ mLastStickerRescan = SystemClock.elapsedRealtime();
+ mStickerScanExecutor.execute(() -> {
+ try {
+ for (File file : Files.fileTraverser().breadthFirst(stickerDir())) {
+ try {
+ if (file.isFile() && file.canRead()) {
+ DownloadableFile df = new DownloadableFile(file.getAbsolutePath());
+ Drawable icon = fileBackend.getThumbnail(df, getResources(), (int) (getResources().getDisplayMetrics().density * 288), false);
+ if (Build.VERSION.SDK_INT >= 28 && icon instanceof AnimatedImageDrawable) {
+ // Animated drawable not working in spans for me yet
+ // https://stackoverflow.com/questions/76870075/using-animatedimagedrawable-inside-imagespan-renders-wrong-size
+ continue;
+ }
+ final String filename = Files.getNameWithoutExtension(df.getName());
+ Cid[] cids = fileBackend.calculateCids(new FileInputStream(df));
+ emojiSearch.addEmoji(new EmojiSearch.CustomEmoji(filename, cids[0].toString(), icon));
+ }
+ } catch (final Exception e) {
+ Log.w(Config.LOGTAG, "rescanStickers: " + e);
+ }
+ }
+ } catch (final Exception e) {
+ Log.w(Config.LOGTAG, "rescanStickers: " + e);
+ }
+ });
+ }
+
public EmojiSearch emojiSearch() {
return emojiSearch;
}
@@ -1374,6 +1423,7 @@ public class XmppConnectionService extends Service {
mForceDuringOnCreate.set(false);
toggleForegroundService();
setupPhoneStateListener();
+ rescanStickers();
}
@@ -34,6 +34,7 @@ import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.text.style.ImageSpan;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@@ -231,6 +232,7 @@ public class ConversationFragment extends XmppFragment
private File savingAsSticker = null;
private EmojiSearch emojiSearch = null;
private EmojiSearchBinding emojiSearchBinding = null;
+ private PopupWindow emojiPopup = null;
private final OnClickListener clickToMuc =
new OnClickListener() {
@@ -896,8 +898,8 @@ public class ConversationFragment extends XmppFragment
commitAttachments();
return;
}
- final Editable text = this.binding.textinput.getText();
- String body = text == null ? "" : text.toString();
+ Editable body = this.binding.textinput.getText();
+ if (body == null) body = new SpannableStringBuilder("");
final Conversation conversation = this.conversation;
if (body.length() == 0 || conversation == null) {
return;
@@ -910,18 +912,40 @@ public class ConversationFragment extends XmppFragment
boolean attention = false;
if (Pattern.compile("\\A@here\\s.*").matcher(body).find()) {
attention = true;
- body = body.replaceFirst("\\A@here\\s+", "");
+ body.delete(0, 6);
+ while (body.length() > 0 && Character.isWhitespace(body.charAt(0))) body.delete(0, 1);
}
if (conversation.getReplyTo() != null) {
- if (Emoticons.isEmoji(body)) {
- message = conversation.getReplyTo().react(body);
+ if (Emoticons.isEmoji(body.toString())) {
+ message = conversation.getReplyTo().react(body.toString());
} else {
message = conversation.getReplyTo().reply();
message.appendBody(body);
}
message.setEncryption(conversation.getNextEncryption());
} else {
- message = new Message(conversation, body, conversation.getNextEncryption());
+ message = new Message(conversation, body.toString(), conversation.getNextEncryption());
+ message.setBody(body);
+ if (message.bodyIsOnlyEmojis()) {
+ SpannableStringBuilder spannable = message.getSpannableBody(null, null);
+ ImageSpan[] imageSpans = spannable.getSpans(0, spannable.length(), ImageSpan.class);
+ if (imageSpans.length == 1) {
+ // Only one inline image, so it's a sticker
+ String source = imageSpans[0].getSource();
+ if (source != null && source.length() > 0 && source.substring(0, 4).equals("cid:")) {
+ try {
+ final Cid cid = BobTransfer.cid(Uri.parse(source));
+ final String url = activity.xmppConnectionService.getUrlForCid(cid);
+ final File f = activity.xmppConnectionService.getFileForCid(cid);
+ if (url != null) {
+ message.setBody("");
+ message.setRelativeFilePath(f.getAbsolutePath());
+ activity.xmppConnectionService.getFileBackend().updateFileParams(message);
+ }
+ } catch (final Exception e) { }
+ }
+ }
+ }
}
message.setThread(conversation.getThread());
if (attention) {
@@ -1381,7 +1405,7 @@ public class ConversationFragment extends XmppFragment
s.replace(lastColon, s.length(), toInsert, 0, toInsert.length());
});
setupEmojiSearch();
- PopupWindow emojiPopup = new PopupWindow(emojiSearchBinding.getRoot(), WindowManager.LayoutParams.MATCH_PARENT, 400);
+ emojiPopup = new PopupWindow(emojiSearchBinding.getRoot(), WindowManager.LayoutParams.MATCH_PARENT, 400);
Handler emojiDebounce = new Handler(Looper.getMainLooper());
binding.textinput.addTextChangedListener(new TextWatcher() {
@Override
@@ -2829,6 +2853,7 @@ public class ConversationFragment extends XmppFragment
this.activity.xmppConnectionService.getNotificationService().setOpenConversation(null);
}
this.reInitRequiredOnStart = true;
+ if (emojiPopup != null) emojiPopup.dismiss();
}
private void updateChatState(final Conversation conversation, final String msg) {
@@ -232,6 +232,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
if (offerToSetupDiallerIntegration()) return;
if (offerToDownloadStickers()) return;
openBatteryOptimizationDialogIfNeeded();
+ xmppConnectionService.rescanStickers();
}
}
@@ -384,7 +384,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setTextIsSelectable(false);
}
- private void displayEmojiMessage(final ViewHolder viewHolder, final String body, final boolean darkBackground) {
+ private void displayEmojiMessage(final ViewHolder viewHolder, final SpannableStringBuilder body, final boolean darkBackground) {
viewHolder.download_button.setVisibility(View.GONE);
viewHolder.audioPlayer.setVisibility(View.GONE);
viewHolder.image.setVisibility(View.GONE);
@@ -394,10 +394,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else {
viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji);
}
- Spannable span = new SpannableString(body);
- float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f;
- span.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- viewHolder.messageBody.setText(span);
+ ImageSpan[] imageSpans = body.getSpans(0, body.length(), ImageSpan.class);
+ float size = imageSpans.length == 1 || Emoticons.isEmoji(body.toString()) ? 3.0f : 2.0f;
+ body.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ viewHolder.messageBody.setText(body);
}
private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) {
@@ -481,6 +481,31 @@ public class MessageAdapter extends ArrayAdapter<Message> {
return startsWithQuote;
}
+ private SpannableStringBuilder getSpannableBody(final Message message) {
+ Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), activity.getThemeResource(R.attr.ic_attach_photo, R.drawable.ic_attach_photo), 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 fallbackImg;
+ }
+ }, fallbackImg);
+ }
+
private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) {
viewHolder.download_button.setVisibility(View.GONE);
viewHolder.image.setVisibility(View.GONE);
@@ -499,32 +524,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
if (message.getBody() != null && !message.getBody().equals("")) {
viewHolder.messageBody.setVisibility(View.VISIBLE);
final String nick = UIHelper.getMessageDisplayName(message);
- Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), activity.getThemeResource(R.attr.ic_attach_photo, R.drawable.ic_attach_photo), null);
- fallbackImg.setBounds(FileBackend.rectForSize(fallbackImg.getIntrinsicWidth(), fallbackImg.getIntrinsicHeight(), (int) (metrics.density * 32)));
- SpannableStringBuilder body = 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);
- } else {
- d = d.getConstantState().newDrawable();
- d.setBounds(FileBackend.rectForSize(d.getIntrinsicWidth(), d.getIntrinsicHeight(), (int) (metrics.density * 32)));
- }
- return d;
- } catch (final IOException e) {
- return fallbackImg;
- }
- }, fallbackImg);
+ SpannableStringBuilder body = getSpannableBody(message);
boolean hasMeCommand = message.hasMeCommand();
if (hasMeCommand) {
body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
@@ -1090,7 +1090,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
darkBackground, type);
}
} else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) {
- displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground);
+ displayEmojiMessage(viewHolder, getSpannableBody(message), darkBackground);
} else {
displayTextMessage(viewHolder, message, darkBackground, type);
}