diff --git a/src/cheogram/java/com/cheogram/android/EmojiSearch.java b/src/cheogram/java/com/cheogram/android/EmojiSearch.java index caba3af63b830bc3f8b5a3f0ae70953f4c47fff6..f08f0a61daf37942d978ca98050728c8b5b43792 100644 --- a/src/cheogram/java/com/cheogram/android/EmojiSearch.java +++ b/src/cheogram/java/com/cheogram/android/EmojiSearch.java @@ -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 find(final String q) { + public synchronized void addEmoji(final Emoji one) { + emoji.add(one); + } + + public synchronized List find(final String q) { final Set emoticon = new TreeSet<>(); for (Emoji e : emoji) { if (e.emoticonMatch(q)) { @@ -75,6 +81,17 @@ public class EmojiSearch { List lst = new ArrayList<>(emoticon); lst.addAll(Lists.transform(result, (r) -> r.getReferent())); + + List 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 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(); } diff --git a/src/cheogram/java/com/cheogram/android/InlineImageSpan.java b/src/cheogram/java/com/cheogram/android/InlineImageSpan.java new file mode 100644 index 0000000000000000000000000000000000000000..82fcff5667eeda95ca9c589729e1c258580c1a13 --- /dev/null +++ b/src/cheogram/java/com/cheogram/android/InlineImageSpan.java @@ -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; + } +} diff --git a/src/cheogram/java/com/cheogram/android/SpannedToXHTML.java b/src/cheogram/java/com/cheogram/android/SpannedToXHTML.java new file mode 100644 index 0000000000000000000000000000000000000000..87a5e09ba5d068d2ed9ea0a340d2b57f5e52f31f --- /dev/null +++ b/src/cheogram/java/com/cheogram/android/SpannedToXHTML.java @@ -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))); + } + } + } + } +} diff --git a/src/cheogram/res/layout/emoji_search_row.xml b/src/cheogram/res/layout/emoji_search_row.xml index 3865ac4ca4dea914b92d73a4d039bac3accf3d07..36cbffe3da009085a5b80b671f4658c11798498a 100644 --- a/src/cheogram/res/layout/emoji_search_row.xml +++ b/src/cheogram/res/layout/emoji_search_row.xml @@ -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"> + + diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 6927358226c5d7cba5a3bc59530e38fe2f9a6d65..8c6adabb0efed71b042400048e00c8d774a0fc5e 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -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; } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index c7d9189e903a16734a47138deaeadeaeea86073f..d790adc57408234fadc2053d4e26d8074b765889 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index e6707c5bced04bddeb3c5c685a3d49b48ee43239..7236f89b11d6523c3ab7685e9f8a9a7e485014e1 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -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(); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 7992425347a41a422b92219269b999ba1f19eb1c..dbadd5af91c49a9e4eddc4deb077f032c1fceb4d 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -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) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 71748d8b25a1d0a937019b585a3c79907cdd1afb..990a601abeee2ca5a2e7854dc6458d0fd1e5d3b2 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -232,6 +232,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio if (offerToSetupDiallerIntegration()) return; if (offerToDownloadStickers()) return; openBatteryOptimizationDialogIfNeeded(); + xmppConnectionService.rescanStickers(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 3f3e949738a891a3882956386f9d4bff5008912b..251a4fe5cb00dc6430f2f9ff8ed8fc40846430df 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -384,7 +384,7 @@ public class MessageAdapter extends ArrayAdapter { 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 { } 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 { 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 { 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 { 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); }