quote as extra line, quote improvements for slidge, only set "note to self" on *copy*, reduce RAM pressure by the cache a bit (Cheogram)

This commit is contained in:
Arne 2023-05-05 23:49:54 +02:00
parent ce4944d230
commit 7e6a359205
14 changed files with 506 additions and 24 deletions

View file

@ -96,8 +96,10 @@ dependencies {
implementation 'com.github.AppIntro:AppIntro:6.2.0' implementation 'com.github.AppIntro:AppIntro:6.2.0'
implementation 'androidx.browser:browser:1.4.0' implementation 'androidx.browser:browser:1.4.0'
implementation 'com.otaliastudios:transcoder:0.9.1' // 0.10.4 seems to be buggy implementation 'com.otaliastudios:transcoder:0.9.1' // 0.10.4 seems to be buggy
implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.github.singpolyma:Better-Link-Movement-Method:4df081e1e4'
implementation project(':libs:AXML') implementation project(':libs:AXML')
implementation 'com.github.ipld:java-cid:v1.3.1'
} }
ext { ext {

View file

@ -0,0 +1,199 @@
package de.monocles.chat;
import android.net.Uri;
import android.util.Base64;
import android.util.Log;
import java.util.Map;
import java.util.HashMap;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import io.ipfs.cid.Cid;
import io.ipfs.multihash.Multihash;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.http.AesGcmURL;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
public class BobTransfer implements Transferable {
protected int status = Transferable.STATUS_OFFER;
protected URI uri;
protected Account account;
protected Jid to;
protected XmppConnectionService xmppConnectionService;
protected static Map<URI, Long> attempts = new HashMap<>();
public static Cid cid(Uri uri) {
if (uri == null || uri.getScheme() == null || !uri.getScheme().equals("cid")) return null;
return cid(uri.getSchemeSpecificPart());
}
public static Cid cid(URI uri) {
if (uri == null || uri.getScheme() == null || !uri.getScheme().equals("cid")) return null;
return cid(uri.getSchemeSpecificPart());
}
public static Cid cid(String bobCid) {
if (!bobCid.contains("@") || !bobCid.contains("+")) return null;
String[] cidParts = bobCid.split("@")[0].split("\\+");
try {
return CryptoHelper.cid(CryptoHelper.hexToBytes(cidParts[1]), cidParts[0]);
} catch (final NoSuchAlgorithmException e) {
return null;
}
}
public static URI uri(Cid cid) throws NoSuchAlgorithmException, URISyntaxException {
return new URI("cid", multihashAlgo(cid.getType()) + "+" + CryptoHelper.bytesToHex(cid.getHash()) + "@bob.xmpp.org", null);
}
private static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException {
final String algo = CryptoHelper.multihashAlgo(type);
if (algo.equals("sha-1")) return "sha1";
return algo;
}
public BobTransfer(URI uri, Account account, Jid to, XmppConnectionService xmppConnectionService) {
this.xmppConnectionService = xmppConnectionService;
this.uri = uri;
this.to = to;
this.account = account;
}
@Override
public boolean start() {
if (status == Transferable.STATUS_DOWNLOADING) return true;
File f = xmppConnectionService.getFileForCid(cid(uri));
if (f != null && f.canRead()) {
finish(f);
return true;
}
if (xmppConnectionService.hasInternetConnection() && attempts.getOrDefault(uri, 0L) + 10000L < System.currentTimeMillis()) {
attempts.put(uri, System.currentTimeMillis());
changeStatus(Transferable.STATUS_DOWNLOADING);
IqPacket request = new IqPacket(IqPacket.TYPE.GET);
request.setTo(to);
final Element dataq = request.addChild("data", "urn:xmpp:bob");
dataq.setAttribute("cid", uri.getSchemeSpecificPart());
xmppConnectionService.sendIqPacket(account, request, (acct, packet) -> {
final Element data = packet.findChild("data", "urn:xmpp:bob");
if (packet.getType() == IqPacket.TYPE.ERROR || data == null) {
Log.d(Config.LOGTAG, "BobTransfer failed: " + packet);
finish(null);
} else {
final String contentType = data.getAttribute("type");
String fileExtension = "dat";
if (contentType != null) {
fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
}
try {
final byte[] bytes = Base64.decode(data.getContent(), Base64.DEFAULT);
File file = xmppConnectionService.getFileBackend().getStorageLocation(new ByteArrayInputStream(bytes), fileExtension);
file.getParentFile().mkdirs();
if (!file.exists() && !file.createNewFile()) {
throw new IOException(file.getAbsolutePath());
}
final OutputStream outputStream = AbstractConnectionManager.createOutputStream(new DownloadableFile(file.getAbsolutePath()), false, false);
if (outputStream != null && bytes != null) {
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
finish(file);
} else {
Log.w(Config.LOGTAG, "Could not write BobTransfer, null outputStream");
finish(null);
}
} catch (final IOException | XmppConnectionService.BlockedMediaException e) {
Log.w(Config.LOGTAG, "Could not write BobTransfer: " + e);
finish(null);
}
}
});
return true;
} else {
return false;
}
}
@Override
public int getStatus() {
return status;
}
@Override
public int getProgress() {
return 0;
}
@Override
public Long getFileSize() {
return null;
}
@Override
public void cancel() {
// No real way to cancel an iq in process...
changeStatus(Transferable.STATUS_CANCELLED);
}
protected void changeStatus(int newStatus) {
status = newStatus;
xmppConnectionService.updateConversationUi();
}
protected void finish(File f) {
if (f != null) xmppConnectionService.updateConversationUi();
}
public static class ForMessage extends BobTransfer {
protected Message message;
public ForMessage(Message message, XmppConnectionService xmppConnectionService) throws URISyntaxException {
super(new URI(message.getFileParams().url), message.getConversation().getAccount(), message.getCounterpart(), xmppConnectionService);
this.message = message;
}
@Override
public void cancel() {
super.cancel();
message.setTransferable(null);
}
@Override
protected void finish(File f) {
if (f != null) {
message.setRelativeFilePath(f.getAbsolutePath());
final boolean privateMessage = message.isPrivateMessage();
message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString(), false);
xmppConnectionService.updateMessage(message);
}
message.setTransferable(null);
super.finish(f);
}
}
}

View file

@ -0,0 +1,9 @@
package de.monocles.chat;
import android.graphics.drawable.Drawable;
import io.ipfs.cid.Cid;
public interface GetThumbnailForCid {
public Drawable getThumbnail(Cid cid);
}

View file

@ -72,6 +72,10 @@ public class Contact implements ListItem, Blockable {
private long mLastseen = 0; private long mLastseen = 0;
private String mLastPresence = null; private String mLastPresence = null;
private RtpCapability.Capability rtpCapability; private RtpCapability.Capability rtpCapability;
public Contact(Contact other) {
this(other.getAccount().getUuid(), other.systemName, other.serverName, other.presenceName, other.jid, other.subscription, other.photoUri, other.systemAccount, other.keys.toString(), other.getAvatar().sha1sum, other.mLastseen, other.mLastPresence, other.groups.toString(), other.rtpCapability);
setAccount(other.getAccount());
}
public Contact(final String account, final String systemName, final String serverName, final String presenceName, public Contact(final String account, final String systemName, final String serverName, final String presenceName,
final Jid jid, final int subscription, final String photoUri, final Jid jid, final int subscription, final String photoUri,

View file

@ -4,9 +4,16 @@ import android.annotation.SuppressLint;
import android.content.ContentValues; import android.content.ContentValues;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Html;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.text.style.ImageSpan;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.view.View;
import eu.siacs.conversations.ui.util.QuoteHelper; import eu.siacs.conversations.ui.util.QuoteHelper;
@ -14,6 +21,8 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteSource; import com.google.common.io.ByteSource;
import com.google.common.primitives.Longs; import com.google.common.primitives.Longs;
import de.monocles.chat.BobTransfer;
import de.monocles.chat.GetThumbnailForCid;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -37,7 +46,6 @@ import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.http.URL; import eu.siacs.conversations.http.URL;
import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.ui.util.PresenceSelector;
import eu.siacs.conversations.ui.util.QuoteHelper;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
@ -51,6 +59,7 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.Tag;
import eu.siacs.conversations.xml.XmlReader; import eu.siacs.conversations.xml.XmlReader;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import io.ipfs.cid.Cid;
public class Message extends AbstractEntity implements AvatarService.Avatarable { public class Message extends AbstractEntity implements AvatarService.Avatarable {
@ -375,10 +384,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
final Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0"); final Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
fallback.addChild("body", "urn:xmpp:fallback:0") fallback.addChild("body", "urn:xmpp:fallback:0")
.setAttribute("start", "0") .setAttribute("start", "0")
.setAttribute("end", "" + m.body.length()); .setAttribute("end", "" + m.body.codePointCount(0, m.body.length()));
m.addPayload(fallback); m.addPayload(fallback);
return m; return m;
} }
public Message react(String emoji) { public Message react(String emoji) {
Set<String> emojis = new HashSet<>(); Set<String> emojis = new HashSet<>();
if (conversation instanceof Conversation) emojis = ((Conversation) conversation).findReactionsTo(replyId(), null); if (conversation instanceof Conversation) emojis = ((Conversation) conversation).findReactionsTo(replyId(), null);
@ -918,9 +928,68 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
public static class MergeSeparator { public static class MergeSeparator {
} }
public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
final Element html = getHtml();
if (html == null || Build.VERSION.SDK_INT < 24) {
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) -> {}
));
// Make images clickable and long-clickable with BetterLinkMovementMethod
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);
ClickableSpan click_span = new ClickableSpan() {
@Override
public void onClick(View widget) { }
};
spannable.setSpan(click_span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// 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 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 SpannableStringBuilder getMergedBody() { public SpannableStringBuilder getMergedBody() {
SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); return getMergedBody(null, null);
}
public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg);
Message current = this; Message current = this;
while (current.mergeable(current.next())) { while (current.mergeable(current.next())) {
current = current.next(); current = current.next();
@ -928,8 +997,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
break; break;
} }
body.append("\n\n"); body.append("\n\n");
body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
body.append(MessageUtils.filterLtrRtl(current.getBody()).trim()); SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
body.append(current.getSpannableBody(thumbnailer, fallbackImg));
} }
return body; return body;
} }

View file

@ -51,6 +51,7 @@ import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.PresenceTemplate; import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.entities.Roster; import eu.siacs.conversations.entities.Roster;
@ -63,6 +64,7 @@ import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.mam.MamReference;
import io.ipfs.cid.Cid;
public class DatabaseBackend extends SQLiteOpenHelper { public class DatabaseBackend extends SQLiteOpenHelper {
@ -1051,6 +1053,43 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return filesPaths; return filesPaths;
} }
public DownloadableFile getFileForCid(Cid cid) {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query("cheogram.cids", new String[]{"path"}, "cid=?", new String[]{cid.toString()}, null, null, null);
DownloadableFile f = null;
if (cursor.moveToNext()) {
f = new DownloadableFile(cursor.getString(0));
}
cursor.close();
return f;
}
public boolean isBlockedMedia(Cid cid) {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query("cheogram.blocked_media", new String[]{"count(*)"}, "cid=?", new String[]{cid.toString()}, null, null, null);
boolean is = false;
if (cursor.moveToNext()) {
is = cursor.getInt(0) > 0;
}
cursor.close();
return is;
}
public void saveCid(Cid cid, File file) {
saveCid(cid, file, null);
}
public void saveCid(Cid cid, File file, String url) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues cv = new ContentValues();
cv.put("cid", cid.toString());
if (file != null) cv.put("path", file.getAbsolutePath());
if (url != null) cv.put("url", url);
if (db.update("monocles.cids", cv, "cid=?", new String[]{cid.toString()}) < 1) {
db.insertWithOnConflict("monocles.cids", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
}
}
public static class FilePath { public static class FilePath {
public final UUID uuid; public final UUID uuid;
public final String path; public final String path;

View file

@ -50,6 +50,7 @@ import androidx.exifinterface.media.ExifInterface;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
@ -98,6 +99,7 @@ import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.Avatar;
import ezvcard.Ezvcard; import ezvcard.Ezvcard;
import ezvcard.VCard; import ezvcard.VCard;
import io.ipfs.cid.Cid;
import me.drakeet.support.toast.ToastCompat; import me.drakeet.support.toast.ToastCompat;
public class FileBackend { public class FileBackend {
@ -1594,8 +1596,10 @@ public class FileBackend {
public void updateFileParams(Message message) { public void updateFileParams(Message message) {
updateFileParams(message, null); updateFileParams(message, null);
} }
public void updateFileParams(final Message message, final String url) { public void updateFileParams(final Message message, final String url) {
updateFileParams(message, url, true);
}
public void updateFileParams(final Message message, String url, boolean updateCids) {
final boolean encrypted = final boolean encrypted =
message.getEncryption() == Message.ENCRYPTION_PGP message.getEncryption() == Message.ENCRYPTION_PGP
|| message.getEncryption() == Message.ENCRYPTION_DECRYPTED; || message.getEncryption() == Message.ENCRYPTION_DECRYPTED;
@ -1953,6 +1957,27 @@ public class FileBackend {
} }
} }
public File getStorageLocation(final InputStream is, final String extension) throws IOException, XmppConnectionService.BlockedMediaException {
final String mime = MimeUtils.guessMimeTypeFromExtension(extension);
Cid[] cids = calculateCids(is);
File file = getStorageLocation(String.format("%s.%s", cids[0], extension), mime);
for (int i = 0; i < cids.length; i++) {
mXmppConnectionService.saveCid(cids[i], file);
}
return file;
}
public Cid[] calculateCids(final Uri uri) throws IOException {
return calculateCids(mXmppConnectionService.getContentResolver().openInputStream(uri));
}
public Cid[] calculateCids(final InputStream is) throws IOException {
try {
return CryptoHelper.cid(is, new String[]{"SHA-256", "SHA-1", "SHA-512"});
} catch (final NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private static class Dimensions { private static class Dimensions {
public final int width; public final int width;
public final int height; public final int height;

View file

@ -71,6 +71,7 @@ import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl; import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus; import net.java.otr4j.session.SessionStatus;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.xmpp.jid.OtrJidHelper; import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
@ -210,6 +211,7 @@ import eu.siacs.conversations.xmpp.pep.PublishOptions;
import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
import io.ipfs.cid.Cid;
import me.leolin.shortcutbadger.ShortcutBadger; import me.leolin.shortcutbadger.ShortcutBadger;
public class XmppConnectionService extends Service { public class XmppConnectionService extends Service {
@ -1453,7 +1455,7 @@ public class XmppConnectionService extends Service {
Resolver.init(this); Resolver.init(this);
this.mRandom = new SecureRandom(); this.mRandom = new SecureRandom();
updateMemorizingTrustmanager(); updateMemorizingTrustmanager();
final int DEFAULT_CACHE_SIZE_PROPORTION = 8; final int DEFAULT_CACHE_SIZE_PROPORTION = 10;
ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = manager.getMemoryClass(); int memoryClass = manager.getMemoryClass();
int memoryClassInKilobytes = memoryClass * 1024; int memoryClassInKilobytes = memoryClass * 1024;
@ -5623,6 +5625,21 @@ public class XmppConnectionService extends Service {
} }
} }
public DownloadableFile getFileForCid(Cid cid) {
return this.databaseBackend.getFileForCid(cid);
}
public void saveCid(Cid cid, File file) throws BlockedMediaException {
saveCid(cid, file, null);
}
public void saveCid(Cid cid, File file, String url) throws BlockedMediaException {
if (this.databaseBackend.isBlockedMedia(cid)) {
throw new BlockedMediaException();
}
this.databaseBackend.saveCid(cid, file, url);
}
public interface OnMamPreferencesFetched { public interface OnMamPreferencesFetched {
void onPreferencesFetched(Element prefs); void onPreferencesFetched(Element prefs);
@ -5779,4 +5796,5 @@ public class XmppConnectionService extends Service {
} }
}).start(); }).start();
} }
public static class BlockedMediaException extends Exception { }
} }

View file

@ -82,6 +82,7 @@ import androidx.core.content.ContextCompat;
import androidx.core.view.inputmethod.InputConnectionCompat; import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat; import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import android.text.SpannableStringBuilder;
import com.google.common.base.Optional; import com.google.common.base.Optional;
@ -101,6 +102,7 @@ import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import eu.siacs.conversations.utils.TimeFrameUtils; import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
@ -972,7 +974,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return; return;
} }
final Editable text = this.binding.textinput.getText(); final Editable text = this.binding.textinput.getText();
final String body = text == null ? "" : text.toString(); String body = text == null ? "" : text.toString();
final Conversation conversation = this.conversation; final Conversation conversation = this.conversation;
if (body.length() == 0 || conversation == null) { if (body.length() == 0 || conversation == null) {
return; return;
@ -982,6 +984,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
final Message message; final Message message;
if (conversation.getCorrectingMessage() == null) { if (conversation.getCorrectingMessage() == null) {
boolean attention = false;
if (Pattern.compile("\\A@here\\s.*").matcher(body).find()) {
attention = true;
body = body.replaceFirst("\\A@here\\s+", "");
}
if (conversation.getReplyTo() != null) { if (conversation.getReplyTo() != null) {
if (Emoticons.isEmoji(body)) { if (Emoticons.isEmoji(body)) {
message = conversation.getReplyTo().react(body); message = conversation.getReplyTo().react(body);
@ -994,11 +1001,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
message = new Message(conversation, body, conversation.getNextEncryption()); message = new Message(conversation, body, conversation.getNextEncryption());
} }
message.setThread(conversation.getThread()); message.setThread(conversation.getThread());
if (attention) {
message.addPayload(new Element("attention", "urn:xmpp:attention:0"));
}
Message.configurePrivateMessage(message); Message.configurePrivateMessage(message);
} else { } else {
message = conversation.getCorrectingMessage(); message = conversation.getCorrectingMessage();
message.putEdited(message.getUuid(), message.getServerMsgId(), message.getBody(), message.getTimeSent());
message.setBody(body); message.setBody(body);
message.setThread(conversation.getThread());
message.putEdited(message.getUuid(), message.getServerMsgId(), message.getBody(), message.getTimeSent());
message.setServerMsgId(null); message.setServerMsgId(null);
message.setUuid(UUID.randomUUID().toString()); message.setUuid(UUID.randomUUID().toString());
} }
@ -1012,6 +1023,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
default: default:
sendMessage(message); sendMessage(message);
} }
setupReply(null);
} }
private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) { private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) {
@ -1353,6 +1365,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
binding.textinput.setRichContentListener(new String[]{"image/*"}, mEditorContentListener); binding.textinput.setRichContentListener(new String[]{"image/*"}, mEditorContentListener);
binding.textSendButton.setOnClickListener(this.mSendButtonListener); binding.textSendButton.setOnClickListener(this.mSendButtonListener);
binding.contextPreviewCancel.setOnClickListener((v) -> {
setupReply(null);
});
binding.textSendButton.setOnLongClickListener(this.mSendButtonLongListener); binding.textSendButton.setOnLongClickListener(this.mSendButtonLongListener);
binding.scrollToBottomButton.setOnClickListener(this.mScrollButtonListener); binding.scrollToBottomButton.setOnClickListener(this.mScrollButtonListener);
binding.recordVoiceButton.setOnClickListener(this.mRecordVoiceButtonListener); binding.recordVoiceButton.setOnClickListener(this.mRecordVoiceButtonListener);
@ -1447,18 +1462,23 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private void quoteMessage(Message message, @Nullable String user) { private void quoteMessage(Message message, @Nullable String user) {
if (message.isGeoUri()) { if (message.isGeoUri()) {
quoteGeoUri(message, user); quoteGeoUri(message, user);
} else if (message.isFileOrImage()) {
quoteMedia(message, user);
} else if (message.isTypeText()) {
final StringBuilder stringBuilder = new StringBuilder();
if (activity.showDateInQuotes()) {
stringBuilder.append(df.format(message.getTimeSent())).append(System.getProperty("line.separator"));
}
stringBuilder.append(MessageUtils.prepareQuote(message));
quoteText(stringBuilder.toString(), user);
} }
setupReply(message);
} }
private void setupReply(Message message) {
conversation.setReplyTo(message);
if (message == null) {
binding.contextPreview.setVisibility(View.GONE);
return;
}
SpannableStringBuilder body = message.getSpannableBody(null, null);
if (message.isFileOrImage() || message.isOOb()) body.append(" 🖼️");
messageListAdapter.handleTextQuotes(body, activity.isDarkTheme());
binding.contextPreviewText.setText(body);
binding.contextPreview.setVisibility(View.VISIBLE);
}
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
//This should cancel any remaining click events that would otherwise trigger links //This should cancel any remaining click events that would otherwise trigger links

View file

@ -997,9 +997,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
this.contacts.add(contact); this.contacts.add(contact);
} }
} }
final Contact self = account.getSelfContact(); final Contact self = new Contact(account.getSelfContact());
self.setSystemName("Note to Self");
if (self.match(this, needle)) { if (self.match(this, needle)) {
self.setSystemName(getString(R.string.note_to_self));
this.contacts.add(self); this.contacts.add(self);
} }

View file

@ -576,7 +576,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
* Applies QuoteSpan to group of lines which starts with > or » characters. * Applies QuoteSpan to group of lines which starts with > or » characters.
* Appends likebreaks and applies DividerSpan to them to show a padding between quote and text. * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
*/ */
private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) { public boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
boolean startsWithQuote = false; boolean startsWithQuote = false;
int quoteDepth = 1; int quoteDepth = 1;
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) { while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {

View file

@ -10,6 +10,8 @@ import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils; import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
@ -30,6 +32,8 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import io.ipfs.cid.Cid;
import io.ipfs.multihash.Multihash;
public final class CryptoHelper { public final class CryptoHelper {
@ -283,4 +287,55 @@ public final class CryptoHelper {
final String u = url.toLowerCase(); final String u = url.toLowerCase();
return !u.contains(" ") && (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("p1s3://")) && u.endsWith(".pgp"); return !u.contains(" ") && (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("p1s3://")) && u.endsWith(".pgp");
} }
public static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException {
switch(type) {
case sha1:
return "sha-1";
case sha2_256:
return "sha-256";
case sha2_512:
return "sha-512";
default:
throw new NoSuchAlgorithmException("" + type);
}
}
public static Multihash.Type multihashType(String algo) throws NoSuchAlgorithmException {
if (algo.equals("SHA-1") || algo.equals("sha-1") || algo.equals("sha1")) {
return Multihash.Type.sha1;
} else if (algo.equals("SHA-256") || algo.equals("sha-256")) {
return Multihash.Type.sha2_256;
} else if (algo.equals("SHA-512") | algo.equals("sha-512")) {
return Multihash.Type.sha2_512;
} else {
throw new NoSuchAlgorithmException(algo);
}
}
public static Cid cid(byte[] digest, String algo) throws NoSuchAlgorithmException {
return Cid.buildCidV1(Cid.Codec.Raw, multihashType(algo), digest);
}
public static Cid[] cid(InputStream in, String[] algo) throws NoSuchAlgorithmException, IOException {
byte[] buf = new byte[4096];
int len;
MessageDigest[] md = new MessageDigest[algo.length];
for (int i = 0; i < md.length; i++) {
md[i] = MessageDigest.getInstance(algo[i]);
}
while ((len = in.read(buf)) != -1) {
for (int i = 0; i < md.length; i++) {
md[i].update(buf, 0, len);
}
}
Cid[] cid = new Cid[md.length];
for (int i = 0; i < cid.length; i++) {
cid[i] = cid(md[i].digest(), algo[i]);
}
return cid;
}
} }

View file

@ -53,6 +53,44 @@
android:visibility="gone" android:visibility="gone"
app:backgroundColor="?attr/colorAccent" /> app:backgroundColor="?attr/colorAccent" />
<LinearLayout
android:id="@+id/context_preview"
android:visibility="gone"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_above="@+id/input"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minHeight="40dp"
android:paddingTop="8dp"
android:paddingLeft="8dp"
android:paddingRight="14dp"
android:orientation="horizontal"
android:background="?attr/color_background_primary">
<ImageView
android:src="?attr/icon_quote"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginRight="8dp"
android:contentDescription="@string/reply_to" />
<TextView
android:id="@+id/context_preview_text"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content" />
<ImageButton
android:id="@+id/context_preview_cancel"
android:layout_width="20dp"
android:layout_height="20dp"
android:padding="0dp"
android:layout_gravity="center_vertical"
android:contentDescription="Cancel"
android:background="?attr/color_background_primary"
android:src="?attr/icon_cancel" />
</LinearLayout>
<RelativeLayout <RelativeLayout
android:id="@+id/input" android:id="@+id/input"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -63,7 +101,7 @@
<RelativeLayout <RelativeLayout
android:id="@+id/textsend" android:id="@+id/textsend"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
@ -71,7 +109,9 @@
android:paddingStart="2dp" android:paddingStart="2dp"
android:paddingLeft="2dp" android:paddingLeft="2dp"
android:paddingTop="2dp" android:paddingTop="2dp"
android:paddingBottom="2dp"> android:paddingBottom="2dp"
android:background="?attr/color_background_primary">
<ImageButton <ImageButton
android:id="@+id/recordVoiceButton" android:id="@+id/recordVoiceButton"

View file

@ -1260,4 +1260,5 @@
<string name="outgoing_call_timestamp">Outgoing call · %s</string> <string name="outgoing_call_timestamp">Outgoing call · %s</string>
<string name="note_to_self">Note to self</string> <string name="note_to_self">Note to self</string>
<string name="retract_message">Retract message</string> <string name="retract_message">Retract message</string>
<string name="reply_to">Reply to</string>
</resources> </resources>