diff --git a/src/cheogram/java/com/cheogram/android/BobTransfer.java b/src/cheogram/java/com/cheogram/android/BobTransfer.java index 1099b143081ea1f5dfa7c6a58e1c3d5fbe4ed60a..680c2bdfed1b0beab3c0fb4fd886bbd62af2f2a1 100644 --- a/src/cheogram/java/com/cheogram/android/BobTransfer.java +++ b/src/cheogram/java/com/cheogram/android/BobTransfer.java @@ -33,6 +33,7 @@ public class BobTransfer implements Transferable { protected XmppConnectionService xmppConnectionService; public static Cid cid(URI uri) { + if (!uri.getScheme().equals("cid")) return null; String bobCid = uri.getSchemeSpecificPart(); if (!bobCid.contains("@") || !bobCid.contains("+")) return null; String[] cidParts = bobCid.split("@")[0].split("\\+"); diff --git a/src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java b/src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java new file mode 100644 index 0000000000000000000000000000000000000000..17e7d9db6129e4268fdce2c2ca5fb667e57098db --- /dev/null +++ b/src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java @@ -0,0 +1,9 @@ +package com.cheogram.android; + +import android.graphics.drawable.Drawable; + +import io.ipfs.cid.Cid; + +public interface GetThumbnailForCid { + public Drawable getThumbnail(Cid cid); +} diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index db7229afaa579387d9533e73f8c25ad7587fbdca..5d84e743416e1a55d49c98081d1bce3880a1776d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -2,10 +2,15 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; +import android.graphics.drawable.Drawable; import android.graphics.Color; +import android.text.Html; import android.text.SpannableStringBuilder; import android.util.Log; +import com.cheogram.android.BobTransfer; +import com.cheogram.android.GetThumbnailForCid; + import com.google.common.io.ByteSource; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; @@ -25,6 +30,8 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.concurrent.CopyOnWriteArraySet; +import io.ipfs.cid.Cid; + import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; @@ -762,8 +769,42 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public static class MergeSeparator { } + public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) { + final Element html = getHtml(); + if (html == null) { + return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim()); + } else { + SpannableStringBuilder spannable = new SpannableStringBuilder(Html.fromHtml( + MessageUtils.filterLtrRtl(html.toString()).trim(), + Html.FROM_HTML_MODE_COMPACT, + (source) -> { + try { + if (thumbnailer == null) return fallbackImg; + Cid cid = BobTransfer.cid(new URI(source)); + if (cid == null) return fallbackImg; + Drawable thumbnail = thumbnailer.getThumbnail(cid); + if (thumbnail == null) return fallbackImg; + return thumbnail; + } catch (final URISyntaxException e) { + return fallbackImg; + } + }, + (opening, tag, output, xmlReader) -> {} + )); + + // https://stackoverflow.com/a/10187511/8611 + int i = spannable.length(); + while(--i >= 0 && Character.isWhitespace(spannable.charAt(i))) { } + return (SpannableStringBuilder) spannable.subSequence(0, i+1); + } + } + public SpannableStringBuilder getMergedBody() { - SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim()); + return getMergedBody(null, null); + } + + public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) { + SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg); Message current = this; while (current.mergeable(current.next())) { current = current.next(); @@ -773,7 +814,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable body.append("\n\n"); body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); - body.append(MessageUtils.filterLtrRtl(current.getBody()).trim()); + body.append(current.getSpannableBody(thumbnailer, fallbackImg)); } return body; } @@ -868,6 +909,18 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.payloads.add(el); } + public Element getHtml() { + 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 null; + } + public List getCommands() { if (this.payloads == null) return null; diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 706b50043a4b1dc2e39b420c496b7ec1af2c1865..3d1440384903f9b26d6de5e9349f0103d778d2a3 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -112,6 +112,7 @@ public abstract class AbstractGenerator { public List getFeatures(Account account) { final XmppConnection connection = account.getXmppConnection(); final ArrayList features = new ArrayList<>(Arrays.asList(FEATURES)); + features.add("http://jabber.org/protocol/xhtml-im"); if (mXmppConnectionService.confirmMessages()) { features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 0945e31a059233e79892b56963c5d75b922f5574..1613f037283e8a4d21a15fb2dbfc5edcdaa83b88 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -434,6 +434,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } boolean notify = false; + Element html = original.findChild("html", "http://jabber.org/protocol/xhtml-im"); + if (html != null && html.findChild("body", "http://www.w3.org/1999/xhtml") == null) { + html = null; + } + if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) { Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'"); return; @@ -472,7 +477,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } - if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) { + if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || html != null) && !isMucStatusMessage) { final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString()); final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false); final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI; @@ -577,12 +582,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece message.setEncryption(Message.ENCRYPTION_DECRYPTED); } } else { - message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status); - if (body.count > 1) { + message = new Message(conversation, body == null ? "HTML-only message" : body.content, Message.ENCRYPTION_NONE, status); + if (body != null && body.count > 1) { message.setBodyLanguage(body.language); } } + if (html != null) message.addPayload(html); message.setSubject(original.findChildContent("subject")); message.setCounterpart(counterpart); message.setRemoteMsgId(remoteMsgId);