~singpolyma/cheogram-android

87ddf94b2e884dbbc03a484b36519187d397d6ff — Stephen Paul Weber 11 months ago 758c85a
Support for storing and displaying XHTML-IM

Only supports images with XEP-0231 compatible cid: URIs, otherwise images render
as a fallback image.  This commit doesn't yet support fetching the images at
all, but can ask a passed-in thumbnailer to figure out getting the image for a
certain Cid.
M src/cheogram/java/com/cheogram/android/BobTransfer.java => src/cheogram/java/com/cheogram/android/BobTransfer.java +1 -0
@@ 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("\\+");

A src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java => src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java +9 -0
@@ 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);
}

M src/main/java/eu/siacs/conversations/entities/Message.java => src/main/java/eu/siacs/conversations/entities/Message.java +55 -2
@@ 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<Element> getCommands() {
        if (this.payloads == null) return null;


M src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java => src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +1 -0
@@ 112,6 112,7 @@ public abstract class AbstractGenerator {
    public List<String> getFeatures(Account account) {
        final XmppConnection connection = account.getXmppConnection();
        final ArrayList<String> features = new ArrayList<>(Arrays.asList(FEATURES));
        features.add("http://jabber.org/protocol/xhtml-im");
        if (mXmppConnectionService.confirmMessages()) {
            features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES));
        }

M src/main/java/eu/siacs/conversations/parser/MessageParser.java => src/main/java/eu/siacs/conversations/parser/MessageParser.java +9 -3
@@ 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);